From 5bc5777db1105a2d04b3d61ec13b1ebb3576c94a Mon Sep 17 00:00:00 2001 From: nathan Date: Tue, 28 May 2024 16:14:52 -0600 Subject: [PATCH] Framework WIP: - Support for non-app tabs (CRM) - Support for multiple etemplate results from a single request - ApplicationInfo interface to manage app info --- kdots/head.tpl | 2 +- kdots/js/EgwFramework.ts | 171 +++++++++++++++++++++++++---- kdots/js/EgwFrameworkApp.styles.ts | 7 +- kdots/js/EgwFrameworkApp.ts | 38 +++++-- kdots/js/app.ts | 8 -- 5 files changed, 184 insertions(+), 42 deletions(-) diff --git a/kdots/head.tpl b/kdots/head.tpl index 28b45cfa82..7dd3b5c071 100644 --- a/kdots/head.tpl +++ b/kdots/head.tpl @@ -46,7 +46,7 @@
- + diff --git a/kdots/js/EgwFramework.ts b/kdots/js/EgwFramework.ts index d717c04a34..ff3433d30d 100644 --- a/kdots/js/EgwFramework.ts +++ b/kdots/js/EgwFramework.ts @@ -95,11 +95,9 @@ export class EgwFramework extends LitElement /** * This is the list of all applications we know about - * - * @type {any[]} */ @property({type: Array, attribute: "application-list"}) - applicationList = []; + applicationList : ApplicationInfo[] = []; private get tabs() : SlTabGroup { return this.shadowRoot.querySelector("sl-tab-group");} @@ -122,19 +120,36 @@ export class EgwFramework extends LitElement { super.firstUpdated(_changedProperties); - // Load hidden apps like status + // Load hidden apps like status, as long as they can be loaded this.applicationList.forEach((app) => { - if(app.status == "5" && app.url) + if(app.status == "5" && app.url && !app.url.match(/menuaction\=none/)) { this.loadApp(app.name); } }); + // Load additional tabs + Object.values(this.tabApps).forEach(app => this.loadApp(app.name)); // Init timer this.egw.add_timer('topmenu_info_timer'); } + /** + * Special tabs that are not directly associated with an application (CRM) + * @type {[]} + * @private + */ + protected get tabApps() : { [id : string] : ApplicationInfo } + { + return JSON.parse(egw.getSessionItem('api', 'fw_tab_apps') || null) || {}; + } + + protected set tabApps(apps : { [id : string] : ApplicationInfo }) + { + egw.setSessionItem('api', 'fw_tab_apps', JSON.stringify(apps)); + } + get egw() : typeof egw { return window.egw ?? { @@ -188,7 +203,7 @@ export class EgwFramework extends LitElement */ public loadApp(appname : string, active = false, url = null) : EgwFrameworkApp { - const existing : EgwFrameworkApp = this.querySelector(`egw-app[name="${appname}"]`); + const existing : EgwFrameworkApp = this.querySelector(`egw-app[id="${appname}"]`); if(existing) { if(active) @@ -202,11 +217,22 @@ export class EgwFramework extends LitElement return existing; } - const app = this.applicationList.find(a => a.name == appname); + const app = this.applicationList.find(a => a.name == appname) ?? + this.tabApps[appname]; + + if(!app) + { + console.log("Cannot load unknown app '" + appname + "'"); + return null; + } let appComponent = document.createElement("egw-app"); appComponent.setAttribute("id", appname); - appComponent.setAttribute("name", appname); + appComponent.setAttribute("name", app.internalName || appname); appComponent.url = url ?? app?.url; + if(app.title) + { + appComponent.title = app.title; + } this.append(appComponent); // App was not in the tab list @@ -217,7 +243,7 @@ export class EgwFramework extends LitElement } // Wait until new tab is there to activate it - if(active) + if(active || app.active) { this.updateComplete.then(() => { @@ -242,7 +268,7 @@ export class EgwFramework extends LitElement */ public linkHandler(_link : string, _app : string) { - //Determine the app string from the application parameter + // Determine the app string from the application parameter let app = null; if(_app && typeof _app == 'string') { @@ -263,13 +289,13 @@ export class EgwFramework extends LitElement // add target flag _link += '&target=_tab'; const appname = app.appName + ":" + btoa(_link); - this.applicationList[appname] = {...app}; - this.applicationList[appname]['name'] = appname; - this.applicationList[appname]['indexUrl'] = _link; - this.applicationList[appname]['tab'] = null; - this.applicationList[appname]['browser'] = null; - this.applicationList[appname]['title'] = 'view'; - app = this.applicationList[appname]; + this.applicationList.push({ + ...app, + name: appname, + url: _link, + title: 'view' + }); + app = this.applicationList[this.applicationList.length - 1]; } this.loadApp(app.name, true, _link); } @@ -289,6 +315,58 @@ export class EgwFramework extends LitElement } } + public tabLinkHandler(_link : string, _extra = { + id: "" + }) + { + const app = this.parseAppFromUrl(_link); + if(app) + { + const appname = app.name + "-" + btoa(_extra.id ? _extra.id : _link).replace(/=/g, 'i'); + if(this.getApplicationByName(appname)) + { + this.loadApp(appname, true, _link); + return appname; + } + + // add target flag + _link += '&fw_target=' + appname; + // create an actual clone of existing app object + let clone = { + ...app, + ..._extra, + //isFrameworkTab: true, ?? + name: appname, + internalName: app.name, + url: _link, + // Need to override to open, base app might already be opened + opened: undefined + }; + // Store only in session + let tabApps = {...this.tabApps}; + tabApps[appname] = clone; + this.tabApps = tabApps; + + /* ?? + this.applications[appname]['sidemenuEntry'] = this.sidemenuUi.addEntry( + this.applications[appname].displayName, this.applications[appname].icon, + function() + { + self.applicationTabNavigate(self.applications[appname], _link, false, -1, null); + }, this.applications[appname], appname); + + */ + this.loadApp(appname, true); + + return appname; + } + else + { + egw_alertHandler("No appropriate target application has been found.", + "Target link: " + _link); + } + } + /** * Open a (centered) popup window with given size and url * @@ -443,14 +521,46 @@ export class EgwFramework extends LitElement } } + /** + * Store last status of tabs + * tab status being used in order to open all previous opened + * tabs and to activate the last active tab + */ private updateTabs(activeTab) { - let appList = []; + //Send the current tab list to the server + let data = this.assembleTabList(activeTab); + + //Serialize the tab list and check whether it really has changed since the last + //submit + var serialized = egw.jsonEncode(data); + if(serialized != this.serializedTabState) + { + this.serializedTabState = serialized; + if(this.tabApps) + { + this._setTabAppsSession(this.tabApps); + } + egw.jsonq('EGroupware\\Api\\Framework\\Ajax::ajax_tab_changed_state', [data]); + } + } + + private assembleTabList(activeTab) + { + let appList = [] Array.from(this.shadowRoot.querySelectorAll("sl-tab-group.egw_fw__open_applications sl-tab")).forEach((tab : SlTab) => { appList.push({appName: tab.panel, active: activeTab.panel == tab.panel}) }); - this.egw.jsonq('EGroupware\\Api\\Framework\\Ajax::ajax_tab_changed_state', [appList]); + return appList; + } + + private _setTabAppsSession(_tabApps) + { + if(_tabApps) + { + egw.setSessionItem('api', 'fw_tab_apps', JSON.stringify(_tabApps)); + } } /** @@ -519,7 +629,7 @@ export class EgwFramework extends LitElement @sl-tab-show=${this.handleApplicationTabShow} @sl-close=${this.handleApplicationTabClose} > - ${repeat(this.applicationList + ${repeat([...this.applicationList, ...Object.values(this.tabApps)] .filter(app => typeof app.opened !== "undefined" && app.status !== "5") .sort((a, b) => a.opened - b.opened), (app) => this._applicationTabTemplate(app))} @@ -546,4 +656,25 @@ export class EgwFramework extends LitElement `; } +} + +/** + * Information we keep and use about each app on the client side + * This might not be limited to actual EGw apps, + */ +export interface ApplicationInfo +{ + /* Internal name, used for reference & indexing. Might not be an egw app, might have extra bits */ + name : string, + /* Must be an egw app, used for the EgwFrameworkApp, preferences, etc. */ + internalName? : string, + icon : string + title : string, + url : string, + /* What type of application (1: normal, 5: loaded but no tab) */ + status : string,// = "1", + /* Is the app open, and at what place in the tab list */ + opened? : number, + /* Is the app currently active */ + active? : boolean// = false } \ No newline at end of file diff --git a/kdots/js/EgwFrameworkApp.styles.ts b/kdots/js/EgwFrameworkApp.styles.ts index dbebcc458d..01c3e4ef8a 100644 --- a/kdots/js/EgwFrameworkApp.styles.ts +++ b/kdots/js/EgwFrameworkApp.styles.ts @@ -137,7 +137,7 @@ export default css` @media (min-width: 600px) { .egw_fw_app__main { grid-template-columns: [start left] min-content [ main] 1fr [right] min-content [end]; - grid-template-rows: [start sub-header] fit-content(2em) [main] auto [footer] fit-content(2em) [end]; + grid-template-rows: [start sub-header] fit-content(2em) [main] auto [footer] fit-content(4em) [end]; } .egw_fw_app__aside { @@ -149,8 +149,11 @@ export default css` overflow-y: auto; } + egw_fw_app__main_content { + display: flex; + } + ::slotted(*) { - height: 100%; } ::slotted(iframe) { diff --git a/kdots/js/EgwFrameworkApp.ts b/kdots/js/EgwFrameworkApp.ts index a0326a2287..0e519205ab 100644 --- a/kdots/js/EgwFrameworkApp.ts +++ b/kdots/js/EgwFrameworkApp.ts @@ -11,6 +11,7 @@ import {HasSlotController} from "../../api/js/etemplate/Et2Widget/slot"; import type {EgwFramework} from "./EgwFramework"; import {etemplate2} from "../../api/js/etemplate/etemplate2"; import {et2_IPrint} from "../../api/js/etemplate/et2_core_interfaces"; +import {repeat} from "lit/directives/repeat.js"; /** * @summary Application component inside EgwFramework @@ -92,6 +93,9 @@ export class EgwFrameworkApp extends LitElement @property({reflect: true}) name = "Application name"; + @property() + title : string = "Application title"; + @property() url = ""; @@ -145,11 +149,14 @@ export class EgwFrameworkApp extends LitElement { this.rightPanelInfo.preferenceWidth = typeof width !== "undefined" ? parseInt(width) : this.rightPanelInfo.defaultWidth; }); + + this.addEventListener("load", this.handleEtemplateLoad); } disconnectedCallback() { super.disconnectedCallback(); + this.removeEventListener("load", this.handleEtemplateLoad); } firstUpdated() @@ -199,22 +206,26 @@ export class EgwFrameworkApp extends LitElement return this.loadingPromise = this.egw.request( this.framework.getMenuaction('ajax_exec', targetUrl, this.name), [targetUrl] - ).then((data : string[]) => + ).then((data : string | string[] | { DOMNodeID? : string } | { DOMNodeID? : string }[]) => { + if(!data) + { + return; + } // Load request returns HTML. Shove it in. if(typeof data == "string" || typeof data == "object" && typeof data[0] == "string") { - render(html`${unsafeHTML(data.join(""))}`, this); + render(html`${unsafeHTML((data).join(""))}`, this); } else { // We got some data, use it - if(data.DOMNodeID) - { - this.id = data.DOMNodeID; - } + const items = (Array.isArray(data) ? data : [data]) + .filter(data => (typeof data.DOMNodeID == "string" && document.querySelector("[id='" + data.DOMNodeID + "']") == null)); + + render(html`${repeat(items, i => i.DOMNodeID, (item) => html` +
`)}`, this); } - this.addEventListener("load", this.handleEtemplateLoad, {once: true}); // Might have just slotted aside content, hasSlotController will requestUpdate() // but we need to do it anyway for translation @@ -367,7 +378,7 @@ export class EgwFrameworkApp extends LitElement get egw() { - return window.egw ?? (this.parentElement).egw ?? null; + return window.egw(this.name) ?? (this.parentElement).egw ?? null; } get framework() : EgwFramework @@ -375,6 +386,11 @@ export class EgwFrameworkApp extends LitElement return this.closest("egw-framework"); } + get appName() : string + { + return this.name; + } + private hasSideContent(side : "left" | "right") { return this.hasSlotController.test(`${side}-header`) || @@ -387,8 +403,8 @@ export class EgwFrameworkApp extends LitElement */ protected handleEtemplateLoad(event) { - const etemplate = etemplate2.getById(this.id); - if(!etemplate) + const etemplate = etemplate2.getById(event.target.id); + if(!etemplate || !event.composedPath().includes(this)) { return; } @@ -557,7 +573,7 @@ export class EgwFrameworkApp extends LitElement >` : nothing } -

${this.egw?.lang(this.name) ?? this.name}

+

${this.title ?? this.egw?.lang(this.name) ?? this.name}

${this.name} main-header diff --git a/kdots/js/app.ts b/kdots/js/app.ts index 38a0376b3c..aae923a579 100644 --- a/kdots/js/app.ts +++ b/kdots/js/app.ts @@ -23,12 +23,4 @@ document.addEventListener('DOMContentLoaded', () => { window.egw.open_link(e.detail.item.value); }); - - /* Listener on placeholder checkbox */ - // TODO: Remove this & the switch - document.querySelector("#placeholders").addEventListener("sl-change", (e) => - { - document.querySelector("egw-framework").classList.toggle("placeholder", e.target.checked); - document.querySelector("egw-app").classList.toggle("placeholder", e.target.checked); - }); }); \ No newline at end of file