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 {Et2Dialog} from "../Et2Dialog/Et2Dialog";
import {SlMenuItem} from "@shoelace-style/shoelace"; import {SlMenuItem} from "@shoelace-style/shoelace";
import {cssImage} from "../Et2Widget/Et2Widget"; import {cssImage} from "../Et2Widget/Et2Widget";
import {Favorite} from "./Favorite";
/** /**
* Favorites widget, designed for use in the nextmatch header * 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 // Favorites are prefixed in preferences
public static readonly PREFIX = "favorite_"; public static readonly PREFIX = "favorite_";
protected static readonly ADD_VALUE = "~add~"; static readonly ADD_VALUE = "~add~";
private favSortedList : any = []; private favSortedList : any = [];
private _preferred : string; private _preferred : string;
@ -193,8 +194,7 @@ export class Et2Favorites extends Et2DropdownButton implements et2_INextmatchHea
if(changedProperties.has("app")) if(changedProperties.has("app"))
{ {
this._preferred = <string>this.egw().preference(this.defaultPref, this.app); this._preferred = <string>this.egw().preference(this.defaultPref, this.app);
this.__select_options = this._load_favorites(this.app); this._load_favorites(this.app);
this.requestUpdate("select_options");
} }
} }
@ -205,87 +205,26 @@ export class Et2Favorites extends Et2DropdownButton implements et2_INextmatchHea
*/ */
_load_favorites(app) _load_favorites(app)
{ {
Favorite.load(this.egw(), app).then((favorites) =>
// 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)
{ {
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); options.push(Object.assign({value: name, label: favorites[name].name || name}, favorites[name]));
favorites[name] = preferences[pref_name]; })
// Keep older favorites working - they used to store nm filters in 'filters',not state // Only add 'Add current' if we have a nextmatch
if(preferences[pref_name]["filters"]) if(this._nextmatch)
{
favorites[pref_name]["state"] = preferences[pref_name]["filters"];
}
}
if(pref_name == 'fav_sort_pref')
{ {
this.favSortedList = preferences[pref_name]; options.push({value: Et2Favorites.ADD_VALUE, label: this.egw().lang('Add current')});
//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(',');
}
} }
} this.__select_options = options
this.requestUpdate("select_options");
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;
} }
public load_favorites(app) public load_favorites(app)
{ {
this.__select_options = this._load_favorites(app); this._load_favorites(app);
this.requestUpdate("select_options");
} }
/** /**
@ -349,7 +288,7 @@ export class Et2Favorites extends Et2DropdownButton implements et2_INextmatchHea
} }
this._value = ev.detail.item.value; 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) 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(); trash.remove();
// Delete preference server side // Delete preference server side
let request = this.egw().json("EGroupware\\Api\\Framework::ajax_set_favorite", Favorite.remove(this.egw(), this.app, line.value).then(response =>
[this.app, fav.name, "delete", "" + fav.group, '']).sendRequest();
request.then(response =>
{ {
line.classList.remove("loading"); line.classList.remove("loading");
@ -442,24 +379,6 @@ export class Et2Favorites extends Et2DropdownButton implements et2_INextmatchHea
return false; 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 * Set the nextmatch to filter
* From et2_INextmatchHeader interface * 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' // Re-generate filter list so we can add 'Add current'
this.__select_options = this._load_favorites(this.app); this._load_favorites(this.app);
this.requestUpdate("select_options");
} }
} }

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/Et2ImageExpose';
import './Expose/Et2DescriptionExpose'; import './Expose/Et2DescriptionExpose';
import './Et2Favorites/Et2Favorites'; import './Et2Favorites/Et2Favorites';
import './Et2Favorites/Et2FavoritesMenu';
import './Et2Image/Et2Image'; import './Et2Image/Et2Image';
import './Et2Image/Et2AppIcon'; import './Et2Image/Et2AppIcon';
import './Et2Avatar/Et2LAvatar'; import './Et2Avatar/Et2LAvatar';

View File

@ -582,7 +582,13 @@ export class EgwFrameworkApp extends LitElement
// No favorites here // No favorites here
if(menu["title"] == "Favorites" || menu["title"] == this.egw.lang("favorites")) 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 // Just one thing, don't bother with submenu
if(menu["entries"].length == 1) if(menu["entries"].length == 1)