2022-05-18 19:01:27 +02:00
|
|
|
/**
|
|
|
|
* EGroupware eTemplate2 - JS Favorite widget
|
|
|
|
*
|
|
|
|
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
|
|
|
|
* @package etemplate
|
|
|
|
* @subpackage api
|
|
|
|
* @link https://www.egroupware.org
|
|
|
|
* @author Nathan Gray
|
|
|
|
* @copyright Nathan Gray 2022
|
|
|
|
*/
|
|
|
|
|
|
|
|
import {Et2DropdownButton} from "../Et2DropdownButton/Et2DropdownButton";
|
|
|
|
import {css, html, PropertyValues, TemplateResult} from "@lion/core";
|
|
|
|
import {SelectOption} from "../Et2Select/FindSelectOptions";
|
|
|
|
import {et2_INextmatchHeader, et2_nextmatch} from "../et2_extension_nextmatch";
|
|
|
|
import {Et2Image} from "../Et2Image/Et2Image";
|
|
|
|
import {Et2Dialog} from "../Et2Dialog/Et2Dialog";
|
|
|
|
import {SlMenuItem} from "@shoelace-style/shoelace";
|
2022-06-28 22:09:29 +02:00
|
|
|
import {cssImage} from "../Et2Widget/Et2Widget";
|
2022-05-18 19:01:27 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Favorites widget, designed for use in the nextmatch header
|
|
|
|
*
|
|
|
|
* The primary control is a split/dropdown button. Clicking on the left side of the button filters the
|
|
|
|
* nextmatch list by the user's default filter. The right side of the button gives a list of
|
|
|
|
* saved filters, pulled from preferences. Clicking a filter from the dropdown list sets the
|
|
|
|
* filters as saved.
|
|
|
|
*
|
|
|
|
* Favorites can also automatically be shown in the sidebox, using the special ID favorite_sidebox.
|
|
|
|
* Use the following code to generate the sidebox section:
|
|
|
|
* display_sidebox($appname,lang('Favorites'),array(
|
|
|
|
* array(
|
|
|
|
* 'no_lang' => true,
|
|
|
|
* 'text'=>'<span id="favorite_sidebox"/>',
|
|
|
|
* 'link'=>false,
|
|
|
|
* 'icon' => false
|
|
|
|
* )
|
|
|
|
* ));
|
|
|
|
* This sidebox list will be automatically generated and kept up to date.
|
|
|
|
*
|
|
|
|
*
|
|
|
|
* Favorites are implemented by saving the values for [column] filters. Filters are stored
|
|
|
|
* in preferences, with the name favorite_<name>. The favorite favorite used for clicking on
|
|
|
|
* the filter button is stored in nextmatch-<columnselection_pref>-favorite.
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
export class Et2Favorites extends Et2DropdownButton implements et2_INextmatchHeader
|
|
|
|
{
|
|
|
|
static get styles()
|
|
|
|
{
|
|
|
|
return [
|
|
|
|
...super.styles,
|
|
|
|
css`
|
2022-08-03 18:14:52 +02:00
|
|
|
:host {
|
|
|
|
min-width: 8ex;
|
|
|
|
}
|
2022-06-28 22:09:29 +02:00
|
|
|
et2-image {
|
|
|
|
height: 2ex;
|
|
|
|
padding: 0px;
|
|
|
|
margin-top: -5px;
|
|
|
|
vertical-align: middle;
|
|
|
|
}
|
|
|
|
et2-image[src="trash"] {
|
|
|
|
display:none;
|
|
|
|
}
|
|
|
|
sl-menu {
|
|
|
|
min-width: 15em;
|
|
|
|
}
|
|
|
|
sl-menu-item:hover et2-image[src="trash"] {
|
|
|
|
display:initial;
|
|
|
|
}
|
|
|
|
/* Add star icons - radio button is already in prefix */
|
|
|
|
sl-menu-item::part(base) {
|
|
|
|
background-image: ${cssImage("fav_filter")};
|
|
|
|
background-repeat: no-repeat;
|
|
|
|
background-size: 16px 16px;
|
|
|
|
background-position: 5px center;
|
|
|
|
}
|
|
|
|
sl-menu-item[checked]::part(base) {
|
|
|
|
background-image: ${cssImage("favorites")};
|
|
|
|
}
|
|
|
|
sl-menu-item:last-child::part(base) {
|
|
|
|
background-image: none;
|
|
|
|
}
|
2022-05-18 19:01:27 +02:00
|
|
|
`,
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
|
|
|
static get properties()
|
|
|
|
{
|
|
|
|
return {
|
|
|
|
...super.properties,
|
|
|
|
// Where we keep the "default" preference
|
2022-07-21 17:57:50 +02:00
|
|
|
defaultPref: {type: String},
|
2022-05-18 19:01:27 +02:00
|
|
|
// Application to show favorites for
|
|
|
|
app: {type: String},
|
|
|
|
// Extra filters to include in the saved favorite
|
|
|
|
filters: {type: Object}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
// Favorites are prefixed in preferences
|
|
|
|
public static readonly PREFIX = "favorite_";
|
|
|
|
protected static readonly ADD_VALUE = "~add~";
|
|
|
|
|
2022-08-04 09:08:46 +02:00
|
|
|
private favSortedList : any = [];
|
2022-05-18 19:01:27 +02:00
|
|
|
private _preferred : string;
|
|
|
|
private _nextmatch : et2_nextmatch;
|
|
|
|
|
|
|
|
constructor()
|
|
|
|
{
|
|
|
|
|
|
|
|
super();
|
|
|
|
this.__statustext = "Favorite queries";
|
|
|
|
this._handleRadio = this._handleRadio.bind(this);
|
|
|
|
this._handleDelete = this._handleDelete.bind(this);
|
|
|
|
}
|
|
|
|
|
|
|
|
connectedCallback()
|
|
|
|
{
|
|
|
|
super.connectedCallback();
|
|
|
|
if(!this.id)
|
|
|
|
{
|
|
|
|
this.id = "favorite";
|
|
|
|
}
|
|
|
|
|
2022-07-21 17:57:50 +02:00
|
|
|
this._preferred = <string>this.egw().preference(this.defaultPref, this.app);
|
2022-05-18 19:01:27 +02:00
|
|
|
|
|
|
|
// Need to wait until update is done and these exist
|
|
|
|
this.updateComplete.then(() =>
|
|
|
|
{
|
|
|
|
if(this.buttonNode)
|
|
|
|
{
|
|
|
|
let img = new Et2Image();
|
|
|
|
img.src = "fav_filter";
|
|
|
|
this.buttonNode.append(img);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
set select_options(_new_options : SelectOption[])
|
|
|
|
{
|
|
|
|
// We don't actually want your options, thanks.
|
|
|
|
}
|
|
|
|
|
|
|
|
get select_options() : SelectOption[]
|
|
|
|
{
|
|
|
|
if(this.__select_options.length)
|
|
|
|
{
|
|
|
|
return this.__select_options;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
_optionTemplate(option : SelectOption) : TemplateResult
|
|
|
|
{
|
|
|
|
let radio = html`<input type="radio" slot="prefix" name="favorite" value="${option.value}"
|
|
|
|
?checked="${option.value == this._preferred}"
|
|
|
|
@change=${this._handleRadio}
|
|
|
|
title="${this.egw().lang('Set as default')}"/>`;
|
|
|
|
|
|
|
|
//@ts-ignore TS doesn't know about window.app
|
2022-06-28 22:09:29 +02:00
|
|
|
let is_admin = (typeof this.egw().app('admin') != "undefined");
|
2022-05-18 19:01:27 +02:00
|
|
|
//@ts-ignore option.group does not exist
|
|
|
|
let icon = (option.group !== false && !is_admin || option.value == 'blank') ? "" : html`
|
|
|
|
<et2-image slot="suffix" src=${"trash"} icon @click=${this._handleDelete}
|
|
|
|
statustext="${this.egw().lang("Delete")}"></et2-image>`;
|
|
|
|
|
|
|
|
return html`
|
2022-06-28 22:09:29 +02:00
|
|
|
<sl-menu-item value="${option.value}" ?checked="${option.value == this._preferred}">
|
2022-05-18 19:01:27 +02:00
|
|
|
${option.value !== Et2Favorites.ADD_VALUE ? radio : ""}
|
|
|
|
${icon}
|
|
|
|
${option.label}
|
|
|
|
</sl-menu-item>`;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** @param {import('@lion/core').PropertyValues } changedProperties */
|
|
|
|
updated(changedProperties : PropertyValues)
|
|
|
|
{
|
|
|
|
super.updated(changedProperties);
|
|
|
|
if(changedProperties.has("app"))
|
|
|
|
{
|
2022-07-21 17:57:50 +02:00
|
|
|
this._preferred = <string>this.egw().preference(this.defaultPref, this.app);
|
2022-05-18 19:01:27 +02:00
|
|
|
this.__select_options = this._load_favorites(this.app);
|
|
|
|
this.requestUpdate("select_options");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Load favorites from preferences
|
|
|
|
*
|
|
|
|
* @param app String Load favorites from this application
|
|
|
|
*/
|
|
|
|
_load_favorites(app)
|
|
|
|
{
|
|
|
|
|
|
|
|
// Default blank filter
|
|
|
|
let favorites : any = {
|
|
|
|
'blank': {
|
|
|
|
name: this.egw().lang("No filters"),
|
|
|
|
state: {}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// Load saved favorites
|
2022-08-04 09:08:46 +02:00
|
|
|
this.favSortedList = [];
|
2022-05-18 19:01:27 +02:00
|
|
|
let preferences : any = this.egw().preference("*", app);
|
|
|
|
for(let pref_name in preferences)
|
|
|
|
{
|
|
|
|
if(pref_name.indexOf(Et2Favorites.PREFIX) == 0 && typeof preferences[pref_name] == 'object')
|
|
|
|
{
|
|
|
|
let name = pref_name.substr(Et2Favorites.PREFIX.length);
|
|
|
|
favorites[name] = preferences[pref_name];
|
|
|
|
// Keep older favorites working - they used to store nm filters in 'filters',not state
|
|
|
|
if(preferences[pref_name]["filters"])
|
|
|
|
{
|
|
|
|
favorites[pref_name]["state"] = preferences[pref_name]["filters"];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if(pref_name == 'fav_sort_pref')
|
|
|
|
{
|
|
|
|
this.favSortedList = preferences[pref_name];
|
|
|
|
//Make sure sorted list is always an array, seems some old fav are not array
|
|
|
|
if(!Array.isArray(this.favSortedList))
|
|
|
|
{
|
|
|
|
this.favSortedList = this.favSortedList.split(',');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for(let name in favorites)
|
|
|
|
{
|
|
|
|
if(this.favSortedList.indexOf(name) < 0)
|
|
|
|
{
|
|
|
|
this.favSortedList.push(name);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this.egw().set_preference(this.app, 'fav_sort_pref', this.favSortedList);
|
|
|
|
if(this.favSortedList.length > 0)
|
|
|
|
{
|
|
|
|
let sortedListObj = {};
|
|
|
|
|
|
|
|
for(let i = 0; i < this.favSortedList.length; i++)
|
|
|
|
{
|
|
|
|
if(typeof favorites[this.favSortedList[i]] != 'undefined')
|
|
|
|
{
|
|
|
|
sortedListObj[this.favSortedList[i]] = favorites[this.favSortedList[i]];
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
this.favSortedList.splice(i, 1);
|
|
|
|
this.egw().set_preference(this.app, 'fav_sort_pref', this.favSortedList);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
favorites = Object.assign(sortedListObj, favorites);
|
|
|
|
}
|
|
|
|
|
|
|
|
let options = [];
|
|
|
|
Object.keys(favorites).forEach((name) =>
|
|
|
|
{
|
|
|
|
options.push(Object.assign({value: name, label: favorites[name].name || name}, favorites[name]));
|
|
|
|
})
|
|
|
|
// Only add 'Add current' if we have a nextmatch
|
|
|
|
if(this._nextmatch)
|
|
|
|
{
|
|
|
|
options.push({value: Et2Favorites.ADD_VALUE, label: this.egw().lang('Add current')});
|
|
|
|
}
|
|
|
|
return options;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Add the current settings as a new favorite
|
|
|
|
*/
|
|
|
|
_add_current()
|
|
|
|
{
|
|
|
|
// Get current filters
|
|
|
|
let current_filters = Object.assign({}, this._nextmatch.activeFilters);
|
|
|
|
|
|
|
|
// Add in extras
|
|
|
|
for(let extra in this.filters)
|
|
|
|
{
|
|
|
|
// Don't overwrite what nm has, chances are nm has more up-to-date value
|
|
|
|
if(typeof current_filters == 'undefined')
|
|
|
|
{
|
|
|
|
// @ts-ignore
|
|
|
|
current_filters[extra] = this._nextmatch.options.settings[extra];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Skip columns
|
|
|
|
delete current_filters.selectcols;
|
|
|
|
|
|
|
|
// Add in application's settings
|
|
|
|
if(this.filters != true)
|
|
|
|
{
|
|
|
|
for(let i = 0; i < this.filters.length; i++)
|
|
|
|
{
|
|
|
|
current_filters[this.filters[i]] = this._nextmatch.options.settings[this.filters[i]];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Call framework
|
|
|
|
//@ts-ignore TS doesn't know about window.app
|
|
|
|
window.app[this.app].add_favorite(current_filters);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get a favorite from the list by id
|
|
|
|
*/
|
|
|
|
favoriteByID(id : string) : any
|
|
|
|
{
|
|
|
|
if(!id)
|
|
|
|
{
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
return this.__select_options.find(f => f.value == id)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Clicked on an option
|
|
|
|
*
|
|
|
|
* @param ev
|
|
|
|
* @protected
|
|
|
|
*/
|
|
|
|
protected _handleSelect(ev)
|
|
|
|
{
|
|
|
|
if(ev.detail.item.value == Et2Favorites.ADD_VALUE)
|
|
|
|
{
|
|
|
|
return this._add_current();
|
|
|
|
}
|
|
|
|
this._value = ev.detail.item.value;
|
|
|
|
|
|
|
|
this._apply_favorite(ev.detail.item.value);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Clicked a radio button
|
|
|
|
*
|
|
|
|
* @param _ev
|
|
|
|
* @protected
|
|
|
|
*/
|
|
|
|
protected _handleRadio(_ev)
|
|
|
|
{
|
|
|
|
// Don't do the menu
|
|
|
|
_ev.stopImmediatePropagation();
|
|
|
|
|
|
|
|
// Save as default favorite - used when you click the button
|
|
|
|
let pref = _ev.target.value;
|
2022-07-21 17:57:50 +02:00
|
|
|
this.egw().set_preference(this.app, this.defaultPref, pref);
|
2022-05-18 19:01:27 +02:00
|
|
|
this._preferred = pref;
|
|
|
|
this.dropdownNode.hide();
|
|
|
|
this.requestUpdate();
|
|
|
|
}
|
|
|
|
|
|
|
|
_handleDelete(_ev : MouseEvent)
|
|
|
|
{
|
|
|
|
// Don't do the menu
|
|
|
|
_ev.stopImmediatePropagation();
|
|
|
|
|
|
|
|
let trash = <HTMLElement>(<HTMLElement>_ev.target).parentNode;
|
|
|
|
let line = <SlMenuItem>trash.parentNode;
|
|
|
|
let fav = this.favoriteByID(line.value);
|
|
|
|
line.classList.add("loading");
|
|
|
|
|
|
|
|
// Make sure first
|
|
|
|
let do_delete = function(button_id)
|
|
|
|
{
|
|
|
|
if(button_id != Et2Dialog.YES_BUTTON)
|
|
|
|
{
|
|
|
|
line.classList.remove('loading');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Hide the trash
|
|
|
|
trash.remove();
|
|
|
|
|
|
|
|
// Delete preference server side
|
|
|
|
let request = this.egw().json("EGroupware\\Api\\Framework::ajax_set_favorite",
|
|
|
|
[this.app, fav.name, "delete", "" + fav.group, '']).sendRequest();
|
|
|
|
request.then(response =>
|
|
|
|
{
|
|
|
|
line.classList.remove("loading");
|
|
|
|
|
|
|
|
let result = response.response.find(r => r.type == "data");
|
|
|
|
|
|
|
|
// Could not find the result we want
|
|
|
|
if(!result || result.type !== "data")
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if(typeof result.data == 'boolean' && result.data)
|
|
|
|
{
|
|
|
|
// Remove line from list
|
|
|
|
line.remove();
|
|
|
|
|
|
|
|
// Remove favorite from options
|
|
|
|
this.__select_options = this.__select_options.filter(f => f.value != fav.value);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
// Something went wrong server side
|
|
|
|
line.classList.add('error');
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}.bind(this);
|
|
|
|
Et2Dialog.show_dialog(do_delete, (this.egw().lang("Delete") + " " + fav.name + "?"),
|
|
|
|
"Delete", null, Et2Dialog.BUTTONS_YES_NO, Et2Dialog.QUESTION_MESSAGE);
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Clicked the main button
|
|
|
|
*
|
|
|
|
* @param {MouseEvent} _ev
|
|
|
|
* @returns {boolean}
|
|
|
|
* @protected
|
|
|
|
*/
|
|
|
|
_handleClick(_ev : MouseEvent) : boolean
|
|
|
|
{
|
|
|
|
// Apply preferred filter - make sure it's an object, and not a reference
|
|
|
|
if(this._preferred && this.favoriteByID(this._preferred))
|
|
|
|
{
|
|
|
|
this._apply_favorite(this._preferred);
|
|
|
|
}
|
|
|
|
_ev.stopImmediatePropagation();
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Apply a favorite to the app or nextmatch
|
|
|
|
*
|
|
|
|
* @param {string} favorite_name
|
|
|
|
* @protected
|
|
|
|
*/
|
|
|
|
protected _apply_favorite(favorite_name : string)
|
|
|
|
{
|
|
|
|
let fav = favorite_name == "blank" ? {} : this.favoriteByID(favorite_name);
|
|
|
|
// use app[appname].setState if available to allow app to overwrite it (eg. change to non-listview in calendar)
|
|
|
|
//@ts-ignore TS doesn't know about window.app
|
|
|
|
if(typeof window.app[this.app] != 'undefined')
|
|
|
|
{
|
|
|
|
//@ts-ignore TS doesn't know about window.app
|
|
|
|
window.app[this.app].setState(fav);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set the nextmatch to filter
|
|
|
|
* From et2_INextmatchHeader interface
|
|
|
|
*
|
|
|
|
* @param {et2_nextmatch} nextmatch
|
|
|
|
*/
|
|
|
|
setNextmatch(nextmatch)
|
|
|
|
{
|
|
|
|
this._nextmatch = nextmatch;
|
|
|
|
|
|
|
|
if(this.nm_filter)
|
|
|
|
{
|
|
|
|
this.set_value(this.nm_filter);
|
|
|
|
this.nm_filter = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Re-generate filter list so we can add 'Add current'
|
|
|
|
this.__select_options = this._load_favorites(this.app);
|
|
|
|
this.requestUpdate("select_options");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// @ts-ignore TypeScript is not recognizing that this is a LitElement
|
|
|
|
customElements.define("et2-favorites", Et2Favorites);
|