mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-11-24 08:53:37 +01:00
Refactor Favorites UI
- Move common stuff into Favorite.ts - New widget Et2FavoritesMenu that's just a menu - Et2Favorite unchanged, still dependent on nextmatch
This commit is contained in:
parent
16c727b84a
commit
95f1034abd
@ -17,6 +17,7 @@ import {Et2Image} from "../Et2Image/Et2Image";
|
||||
import {Et2Dialog} from "../Et2Dialog/Et2Dialog";
|
||||
import {SlMenuItem} from "@shoelace-style/shoelace";
|
||||
import {cssImage} from "../Et2Widget/Et2Widget";
|
||||
import {Favorite} from "./Favorite";
|
||||
|
||||
/**
|
||||
* Favorites widget, designed for use in the nextmatch header
|
||||
@ -108,7 +109,7 @@ export class Et2Favorites extends Et2DropdownButton implements et2_INextmatchHea
|
||||
|
||||
// Favorites are prefixed in preferences
|
||||
public static readonly PREFIX = "favorite_";
|
||||
protected static readonly ADD_VALUE = "~add~";
|
||||
static readonly ADD_VALUE = "~add~";
|
||||
|
||||
private favSortedList : any = [];
|
||||
private _preferred : string;
|
||||
@ -193,8 +194,7 @@ export class Et2Favorites extends Et2DropdownButton implements et2_INextmatchHea
|
||||
if(changedProperties.has("app"))
|
||||
{
|
||||
this._preferred = <string>this.egw().preference(this.defaultPref, this.app);
|
||||
this.__select_options = this._load_favorites(this.app);
|
||||
this.requestUpdate("select_options");
|
||||
this._load_favorites(this.app);
|
||||
}
|
||||
}
|
||||
|
||||
@ -205,87 +205,26 @@ export class Et2Favorites extends Et2DropdownButton implements et2_INextmatchHea
|
||||
*/
|
||||
_load_favorites(app)
|
||||
{
|
||||
|
||||
// Default blank filter
|
||||
let favorites : any = {
|
||||
'blank': {
|
||||
name: this.egw().lang("No filters"),
|
||||
state: {}
|
||||
}
|
||||
};
|
||||
|
||||
// Load saved favorites
|
||||
this.favSortedList = [];
|
||||
let preferences : any = this.egw().preference("*", app);
|
||||
for(let pref_name in preferences)
|
||||
Favorite.load(this.egw(), app).then((favorites) =>
|
||||
{
|
||||
if(pref_name.indexOf(Et2Favorites.PREFIX) == 0 && typeof preferences[pref_name] == 'object')
|
||||
let options = [];
|
||||
Object.keys(favorites).forEach((name) =>
|
||||
{
|
||||
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')
|
||||
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)
|
||||
{
|
||||
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(',');
|
||||
}
|
||||
options.push({value: Et2Favorites.ADD_VALUE, label: this.egw().lang('Add current')});
|
||||
}
|
||||
}
|
||||
|
||||
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')});
|
||||
}
|
||||
|
||||
this.requestUpdate("select_options");
|
||||
return options;
|
||||
this.__select_options = options
|
||||
this.requestUpdate("select_options");
|
||||
});
|
||||
}
|
||||
|
||||
public load_favorites(app)
|
||||
{
|
||||
this.__select_options = this._load_favorites(app);
|
||||
this.requestUpdate("select_options");
|
||||
this._load_favorites(app);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -349,7 +288,7 @@ export class Et2Favorites extends Et2DropdownButton implements et2_INextmatchHea
|
||||
}
|
||||
this._value = ev.detail.item.value;
|
||||
|
||||
this._apply_favorite(ev.detail.item.value);
|
||||
Favorite.applyFavorite(this.egw(), this.app, ev.detail.item.value);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -360,7 +299,7 @@ export class Et2Favorites extends Et2DropdownButton implements et2_INextmatchHea
|
||||
*/
|
||||
protected _handleClick(event : MouseEvent)
|
||||
{
|
||||
this._apply_favorite(this.preferred);
|
||||
Favorite.applyFavorite(this.egw, this.app, this.preferred);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -407,9 +346,7 @@ export class Et2Favorites extends Et2DropdownButton implements et2_INextmatchHea
|
||||
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 =>
|
||||
Favorite.remove(this.egw(), this.app, line.value).then(response =>
|
||||
{
|
||||
line.classList.remove("loading");
|
||||
|
||||
@ -442,24 +379,6 @@ export class Et2Favorites extends Et2DropdownButton implements et2_INextmatchHea
|
||||
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
|
||||
@ -477,8 +396,7 @@ export class Et2Favorites extends Et2DropdownButton implements et2_INextmatchHea
|
||||
}
|
||||
|
||||
// Re-generate filter list so we can add 'Add current'
|
||||
this.__select_options = this._load_favorites(this.app);
|
||||
this.requestUpdate("select_options");
|
||||
this._load_favorites(this.app);
|
||||
}
|
||||
}
|
||||
|
||||
|
115
api/js/etemplate/Et2Favorites/Et2FavoritesMenu.ts
Normal file
115
api/js/etemplate/Et2Favorites/Et2FavoritesMenu.ts
Normal file
@ -0,0 +1,115 @@
|
||||
import {css, html, LitElement, TemplateResult} from "lit";
|
||||
import {customElement} from "lit/decorators/custom-element.js";
|
||||
import {Et2Widget} from "../Et2Widget/Et2Widget";
|
||||
import {Favorite} from "./Favorite";
|
||||
import {property} from "lit/decorators/property.js";
|
||||
import {until} from "lit/directives/until.js";
|
||||
import {repeat} from "lit/directives/repeat.js";
|
||||
|
||||
@customElement("et2-favorites-menu")
|
||||
export class Et2FavoritesMenu extends Et2Widget(LitElement)
|
||||
{
|
||||
|
||||
static get styles()
|
||||
{
|
||||
return [
|
||||
super.styles,
|
||||
css`
|
||||
:host {
|
||||
min-width: 15em;
|
||||
}
|
||||
|
||||
et2-image[src="trash"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
sl-menu-item:hover et2-image[src="trash"] {
|
||||
display: initial;
|
||||
}`
|
||||
]
|
||||
};
|
||||
|
||||
@property()
|
||||
application : string;
|
||||
|
||||
private favorites : { [name : string] : Favorite }
|
||||
private loadingPromise = Promise.resolve();
|
||||
|
||||
connectedCallback()
|
||||
{
|
||||
super.connectedCallback();
|
||||
|
||||
if(this.application)
|
||||
{
|
||||
this.loadingPromise = Favorite.load(this.egw(), this.application).then((favorites) =>
|
||||
{
|
||||
this.favorites = favorites;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleSelect(event)
|
||||
{
|
||||
Favorite.applyFavorite(this.egw(), this.application, event.detail.item.value);
|
||||
}
|
||||
|
||||
handleDelete(event)
|
||||
{
|
||||
// Don't trigger click
|
||||
event.stopPropagation();
|
||||
|
||||
const menuItem = event.target.closest("sl-menu-item");
|
||||
menuItem.setAttribute("loading", "");
|
||||
|
||||
const favoriteName = menuItem.value;
|
||||
|
||||
// Remove from server
|
||||
Favorite.remove(this.egw(), this.application, favoriteName).then(() =>
|
||||
{
|
||||
// Remove from widget
|
||||
delete this.favorites[favoriteName];
|
||||
this.requestUpdate();
|
||||
});
|
||||
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
protected menuItemTemplate(name : string, favorite : Favorite) : TemplateResult
|
||||
{
|
||||
let is_admin = (typeof this.egw().app('admin') != "undefined");
|
||||
|
||||
//@ts-ignore option.group does not exist
|
||||
let icon = (favorite.group !== false && !is_admin || ['blank', '~add~'].includes(name)) ? "" : html`
|
||||
<et2-image slot="suffix" src="trash" icon @click=${this.handleDelete}
|
||||
statustext="${this.egw().lang("Delete")}"></et2-image>`;
|
||||
|
||||
return html`
|
||||
<sl-menu-item value="${name}">
|
||||
${icon}
|
||||
${favorite.name}
|
||||
</sl-menu-item>`;
|
||||
}
|
||||
|
||||
protected loadingTemplate()
|
||||
{
|
||||
return html`
|
||||
<sl-menu-item loading>${this.egw().lang("Loading")}</sl-menu-item>`;
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
let content = this.loadingPromise.then(() =>
|
||||
{
|
||||
return html`
|
||||
<sl-menu
|
||||
@sl-select=${this.handleSelect}
|
||||
>
|
||||
${repeat(Object.keys(this.favorites), (i) => this.menuItemTemplate(i, this.favorites[i]))}
|
||||
</sl-menu>
|
||||
`;
|
||||
});
|
||||
return html`
|
||||
${until(content, this.loadingTemplate())}
|
||||
`;
|
||||
}
|
||||
}
|
109
api/js/etemplate/Et2Favorites/Favorite.ts
Normal file
109
api/js/etemplate/Et2Favorites/Favorite.ts
Normal file
@ -0,0 +1,109 @@
|
||||
export class Favorite
|
||||
{
|
||||
name : string
|
||||
state : object
|
||||
group : number | false
|
||||
|
||||
// Favorites are prefixed in preferences
|
||||
public static readonly PREFIX = "favorite_";
|
||||
public static readonly ADD_VALUE = "~add~";
|
||||
|
||||
/**
|
||||
* Load favorites from preferences
|
||||
*
|
||||
* @param app String Load favorites from this application
|
||||
*/
|
||||
static async load(egw, app : string) : Promise<{ [name : string] : Favorite }>
|
||||
{
|
||||
// Default blank filter
|
||||
let favorites : { [name : string] : Favorite } = {
|
||||
'blank': {
|
||||
name: egw.lang("No filters"),
|
||||
state: {},
|
||||
group: false
|
||||
}
|
||||
};
|
||||
|
||||
// Load saved favorites
|
||||
let sortedList = [];
|
||||
let preferences : any = await egw.preference("*", app, true);
|
||||
for(let pref_name in preferences)
|
||||
{
|
||||
if(pref_name.indexOf(Favorite.PREFIX) == 0 && typeof preferences[pref_name] == 'object')
|
||||
{
|
||||
let name = pref_name.substr(Favorite.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')
|
||||
{
|
||||
sortedList = preferences[pref_name];
|
||||
//Make sure sorted list is always an array, seems some old fav are not array
|
||||
if(!Array.isArray(sortedList) && typeof sortedList == "string")
|
||||
{
|
||||
// @ts-ignore What's the point of a typecheck if IDE still errors
|
||||
sortedList = sortedList.split(',');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for(let name in favorites)
|
||||
{
|
||||
if(sortedList.indexOf(name) < 0)
|
||||
{
|
||||
sortedList.push(name);
|
||||
}
|
||||
}
|
||||
egw.set_preference(app, 'fav_sort_pref', sortedList);
|
||||
if(sortedList.length > 0)
|
||||
{
|
||||
let sortedListObj = {};
|
||||
|
||||
for(let i = 0; i < sortedList.length; i++)
|
||||
{
|
||||
if(typeof favorites[sortedList[i]] != 'undefined')
|
||||
{
|
||||
sortedListObj[sortedList[i]] = favorites[sortedList[i]];
|
||||
}
|
||||
else
|
||||
{
|
||||
sortedList.splice(i, 1);
|
||||
egw.set_preference(app, 'fav_sort_pref', sortedList);
|
||||
}
|
||||
}
|
||||
favorites = Object.assign(sortedListObj, favorites);
|
||||
}
|
||||
|
||||
return favorites;
|
||||
}
|
||||
|
||||
static async applyFavorite(egw, app : string, favoriteName : string)
|
||||
{
|
||||
const favorites = await Favorite.load(egw, app);
|
||||
let fav = favoriteName == "blank" ? {} : favorites[favoriteName] ?? {};
|
||||
// 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[app] != 'undefined')
|
||||
{
|
||||
//@ts-ignore TS doesn't know about window.app
|
||||
window.app[app].setState(fav);
|
||||
}
|
||||
}
|
||||
|
||||
static async remove(egw, app, favoriteName)
|
||||
{
|
||||
const favorites = await Favorite.load(egw, app);
|
||||
let fav = favorites[favoriteName];
|
||||
if(!fav)
|
||||
{
|
||||
return Promise.reject("No such favorite");
|
||||
}
|
||||
|
||||
return egw.request("EGroupware\\Api\\Framework::ajax_set_favorite",
|
||||
[app, favoriteName, "delete", "" + fav.group, '']);
|
||||
}
|
||||
}
|
@ -55,6 +55,7 @@ import './Et2Email/Et2Email';
|
||||
import './Expose/Et2ImageExpose';
|
||||
import './Expose/Et2DescriptionExpose';
|
||||
import './Et2Favorites/Et2Favorites';
|
||||
import './Et2Favorites/Et2FavoritesMenu';
|
||||
import './Et2Image/Et2Image';
|
||||
import './Et2Image/Et2AppIcon';
|
||||
import './Et2Avatar/Et2LAvatar';
|
||||
|
@ -582,7 +582,13 @@ export class EgwFrameworkApp extends LitElement
|
||||
// No favorites here
|
||||
if(menu["title"] == "Favorites" || menu["title"] == this.egw.lang("favorites"))
|
||||
{
|
||||
return nothing;
|
||||
return html`
|
||||
<sl-menu-item>
|
||||
<et2-image style="width:1em;" src="fav_filter" slot="prefix"></et2-image>
|
||||
${menu["title"]}
|
||||
<et2-favorites-menu slot="submenu" application="${this.appName}"></et2-favorites-menu>
|
||||
</sl-menu-item>
|
||||
`;
|
||||
}
|
||||
// Just one thing, don't bother with submenu
|
||||
if(menu["entries"].length == 1)
|
||||
|
Loading…
Reference in New Issue
Block a user