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:
nathan 2024-06-12 11:48:50 -06:00
parent 16c727b84a
commit 95f1034abd
5 changed files with 251 additions and 102 deletions

View File

@ -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);
}
}

View 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())}
`;
}
}

View 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, '']);
}
}

View File

@ -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';

View File

@ -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)