mirror of
https://github.com/EGroupware/egroupware.git
synced 2025-01-03 04:29:28 +01:00
Et2Favorites
This commit is contained in:
parent
5edd7dc782
commit
776ce7202a
@ -44,7 +44,8 @@ export class Et2DropdownButton extends Et2widgetWithSelectMixin(Et2Button)
|
||||
/* Avoid unwanted style overlap from button */
|
||||
border: none;
|
||||
background-color: none;
|
||||
|
||||
}
|
||||
:host, sl-menu {
|
||||
/**
|
||||
Adapt shoelace color variables to what we want
|
||||
Maybe some logical variables from etemplate2.css here?
|
||||
@ -53,6 +54,7 @@ export class Et2DropdownButton extends Et2widgetWithSelectMixin(Et2Button)
|
||||
--sl-color-primary-100: var(--gray-10);
|
||||
--sl-color-primary-300: var(--input-border-color);
|
||||
--sl-color-primary-400: var(--input-border-color);
|
||||
--sl-color-primary-600: var(--primary-background-color);
|
||||
--sl-color-primary-700: #505050;
|
||||
}
|
||||
:host(:active), :host([active]) {
|
||||
@ -129,7 +131,7 @@ export class Et2DropdownButton extends Et2widgetWithSelectMixin(Et2Button)
|
||||
`;
|
||||
}
|
||||
|
||||
protected _optionTemplate(option : SelectOption) : TemplateResult
|
||||
_optionTemplate(option : SelectOption) : TemplateResult
|
||||
{
|
||||
let icon = option.icon ? html`
|
||||
<et2-image slot="prefix" src=${option.icon} icon></et2-image>` : '';
|
||||
@ -143,7 +145,6 @@ export class Et2DropdownButton extends Et2widgetWithSelectMixin(Et2Button)
|
||||
|
||||
protected _handleSelect(ev)
|
||||
{
|
||||
let oldValue = this._value;
|
||||
this._value = ev.detail.item.value;
|
||||
|
||||
// Trigger a change event
|
||||
@ -164,7 +165,7 @@ export class Et2DropdownButton extends Et2widgetWithSelectMixin(Et2Button)
|
||||
this.requestUpdate("value", oldValue);
|
||||
}
|
||||
|
||||
get _optionTargetNode()
|
||||
get _optionTargetNode() : HTMLElement
|
||||
{
|
||||
return this.shadowRoot.querySelector("sl-menu");
|
||||
}
|
||||
|
456
api/js/etemplate/Et2Favorites/Et2Favorites.ts
Normal file
456
api/js/etemplate/Et2Favorites/Et2Favorites.ts
Normal file
@ -0,0 +1,456 @@
|
||||
/**
|
||||
* 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";
|
||||
|
||||
/**
|
||||
* 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`
|
||||
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;
|
||||
}
|
||||
.menu-item__chevron {
|
||||
display:none;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
static get properties()
|
||||
{
|
||||
return {
|
||||
...super.properties,
|
||||
// Where we keep the "default" preference
|
||||
default_pref: {type: String},
|
||||
// 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~";
|
||||
|
||||
private favSortedList : any = null;
|
||||
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";
|
||||
}
|
||||
|
||||
this._preferred = <string>this.egw().preference(this.default_pref, this.app);
|
||||
|
||||
// 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
|
||||
let is_admin = (typeof window.app['admin'] != "undefined");
|
||||
//@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`
|
||||
<sl-menu-item value="${option.value}">
|
||||
${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"))
|
||||
{
|
||||
this._preferred = <string>this.egw().preference(this.default_pref, this.app);
|
||||
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
|
||||
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;
|
||||
this.egw().set_preference(this.app, this.default_pref, pref);
|
||||
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);
|
@ -40,6 +40,7 @@ import './Et2Dialog/Et2Dialog';
|
||||
import './Et2DropdownButton/Et2DropdownButton';
|
||||
import './Expose/Et2ImageExpose';
|
||||
import './Expose/Et2DescriptionExpose';
|
||||
import './Et2Favorites/Et2Favorites';
|
||||
import './Et2Image/Et2Image';
|
||||
import './Et2Link/Et2Link';
|
||||
import './Et2Link/Et2LinkList';
|
||||
|
Loading…
Reference in New Issue
Block a user