From 95f1034abdccfaf2c7a429a35afdd74c0bc88f2b Mon Sep 17 00:00:00 2001 From: nathan Date: Wed, 12 Jun 2024 11:48:50 -0600 Subject: [PATCH] Refactor Favorites UI - Move common stuff into Favorite.ts - New widget Et2FavoritesMenu that's just a menu - Et2Favorite unchanged, still dependent on nextmatch --- api/js/etemplate/Et2Favorites/Et2Favorites.ts | 120 +++--------------- .../Et2Favorites/Et2FavoritesMenu.ts | 115 +++++++++++++++++ api/js/etemplate/Et2Favorites/Favorite.ts | 109 ++++++++++++++++ api/js/etemplate/etemplate2.ts | 1 + kdots/js/EgwFrameworkApp.ts | 8 +- 5 files changed, 251 insertions(+), 102 deletions(-) create mode 100644 api/js/etemplate/Et2Favorites/Et2FavoritesMenu.ts create mode 100644 api/js/etemplate/Et2Favorites/Favorite.ts diff --git a/api/js/etemplate/Et2Favorites/Et2Favorites.ts b/api/js/etemplate/Et2Favorites/Et2Favorites.ts index 652830cf7d..8379fdd21b 100644 --- a/api/js/etemplate/Et2Favorites/Et2Favorites.ts +++ b/api/js/etemplate/Et2Favorites/Et2Favorites.ts @@ -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 = 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); } } diff --git a/api/js/etemplate/Et2Favorites/Et2FavoritesMenu.ts b/api/js/etemplate/Et2Favorites/Et2FavoritesMenu.ts new file mode 100644 index 0000000000..0c42dec913 --- /dev/null +++ b/api/js/etemplate/Et2Favorites/Et2FavoritesMenu.ts @@ -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` + `; + + return html` + + ${icon} + ${favorite.name} + `; + } + + protected loadingTemplate() + { + return html` + ${this.egw().lang("Loading")}`; + } + + render() + { + let content = this.loadingPromise.then(() => + { + return html` + + ${repeat(Object.keys(this.favorites), (i) => this.menuItemTemplate(i, this.favorites[i]))} + + `; + }); + return html` + ${until(content, this.loadingTemplate())} + `; + } +} \ No newline at end of file diff --git a/api/js/etemplate/Et2Favorites/Favorite.ts b/api/js/etemplate/Et2Favorites/Favorite.ts new file mode 100644 index 0000000000..ea7fa1dd7a --- /dev/null +++ b/api/js/etemplate/Et2Favorites/Favorite.ts @@ -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, '']); + } +} \ No newline at end of file diff --git a/api/js/etemplate/etemplate2.ts b/api/js/etemplate/etemplate2.ts index ff638b39ba..7350f6ee4e 100644 --- a/api/js/etemplate/etemplate2.ts +++ b/api/js/etemplate/etemplate2.ts @@ -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'; diff --git a/kdots/js/EgwFrameworkApp.ts b/kdots/js/EgwFrameworkApp.ts index c409ba5be9..d191e3a624 100644 --- a/kdots/js/EgwFrameworkApp.ts +++ b/kdots/js/EgwFrameworkApp.ts @@ -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` + + + ${menu["title"]} + + + `; } // Just one thing, don't bother with submenu if(menu["entries"].length == 1)