Framework WIP:

- Support for non-app tabs (CRM)
- Support for multiple etemplate results from a single request
- ApplicationInfo interface to manage app info
This commit is contained in:
nathan 2024-05-28 16:14:52 -06:00
parent 7448377e96
commit 5bc5777db1
5 changed files with 184 additions and 42 deletions

View File

@ -46,7 +46,7 @@
<div slot="status" id="egw_fw_sidebar_r"></div> <div slot="status" id="egw_fw_sidebar_r"></div>
<!-- Currently open app --> <!-- Currently open app -->
<egw-app name="{open_app_name}" url="{open_app_url}" active></egw-app> <egw-app id="{open_app_name}" name="{open_app_name}" url="{open_app_url}" active></egw-app>
</egw-framework> </egw-framework>

View File

@ -95,11 +95,9 @@ export class EgwFramework extends LitElement
/** /**
* This is the list of all applications we know about * This is the list of all applications we know about
*
* @type {any[]}
*/ */
@property({type: Array, attribute: "application-list"}) @property({type: Array, attribute: "application-list"})
applicationList = []; applicationList : ApplicationInfo[] = [];
private get tabs() : SlTabGroup { return this.shadowRoot.querySelector("sl-tab-group");} private get tabs() : SlTabGroup { return this.shadowRoot.querySelector("sl-tab-group");}
@ -122,19 +120,36 @@ export class EgwFramework extends LitElement
{ {
super.firstUpdated(_changedProperties); super.firstUpdated(_changedProperties);
// Load hidden apps like status // Load hidden apps like status, as long as they can be loaded
this.applicationList.forEach((app) => 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); this.loadApp(app.name);
} }
}); });
// Load additional tabs
Object.values(this.tabApps).forEach(app => this.loadApp(app.name));
// Init timer // Init timer
this.egw.add_timer('topmenu_info_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 get egw() : typeof egw
{ {
return window.egw ?? <typeof egw>{ return window.egw ?? <typeof egw>{
@ -188,7 +203,7 @@ export class EgwFramework extends LitElement
*/ */
public loadApp(appname : string, active = false, url = null) : EgwFrameworkApp 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(existing)
{ {
if(active) if(active)
@ -202,11 +217,22 @@ export class EgwFramework extends LitElement
return existing; 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 = <EgwFrameworkApp>document.createElement("egw-app"); let appComponent = <EgwFrameworkApp>document.createElement("egw-app");
appComponent.setAttribute("id", appname); appComponent.setAttribute("id", appname);
appComponent.setAttribute("name", appname); appComponent.setAttribute("name", app.internalName || appname);
appComponent.url = url ?? app?.url; appComponent.url = url ?? app?.url;
if(app.title)
{
appComponent.title = app.title;
}
this.append(appComponent); this.append(appComponent);
// App was not in the tab list // 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 // Wait until new tab is there to activate it
if(active) if(active || app.active)
{ {
this.updateComplete.then(() => this.updateComplete.then(() =>
{ {
@ -242,7 +268,7 @@ export class EgwFramework extends LitElement
*/ */
public linkHandler(_link : string, _app : string) 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; let app = null;
if(_app && typeof _app == 'string') if(_app && typeof _app == 'string')
{ {
@ -263,13 +289,13 @@ export class EgwFramework extends LitElement
// add target flag // add target flag
_link += '&target=_tab'; _link += '&target=_tab';
const appname = app.appName + ":" + btoa(_link); const appname = app.appName + ":" + btoa(_link);
this.applicationList[appname] = {...app}; this.applicationList.push({
this.applicationList[appname]['name'] = appname; ...app,
this.applicationList[appname]['indexUrl'] = _link; name: appname,
this.applicationList[appname]['tab'] = null; url: _link,
this.applicationList[appname]['browser'] = null; title: 'view'
this.applicationList[appname]['title'] = 'view'; });
app = this.applicationList[appname]; app = this.applicationList[this.applicationList.length - 1];
} }
this.loadApp(app.name, true, _link); 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 * 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) 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) => 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}) 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-tab-show=${this.handleApplicationTabShow}
@sl-close=${this.handleApplicationTabClose} @sl-close=${this.handleApplicationTabClose}
> >
${repeat(this.applicationList ${repeat([...this.applicationList, ...Object.values(this.tabApps)]
.filter(app => typeof app.opened !== "undefined" && app.status !== "5") .filter(app => typeof app.opened !== "undefined" && app.status !== "5")
.sort((a, b) => a.opened - b.opened), (app) => this._applicationTabTemplate(app))} .sort((a, b) => a.opened - b.opened), (app) => this._applicationTabTemplate(app))}
</sl-tab-group> </sl-tab-group>
@ -547,3 +657,24 @@ 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
}

View File

@ -137,7 +137,7 @@ export default css`
@media (min-width: 600px) { @media (min-width: 600px) {
.egw_fw_app__main { .egw_fw_app__main {
grid-template-columns: [start left] min-content [ main] 1fr [right] min-content [end]; 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 { .egw_fw_app__aside {
@ -149,8 +149,11 @@ export default css`
overflow-y: auto; overflow-y: auto;
} }
egw_fw_app__main_content {
display: flex;
}
::slotted(*) { ::slotted(*) {
height: 100%;
} }
::slotted(iframe) { ::slotted(iframe) {

View File

@ -11,6 +11,7 @@ import {HasSlotController} from "../../api/js/etemplate/Et2Widget/slot";
import type {EgwFramework} from "./EgwFramework"; import type {EgwFramework} from "./EgwFramework";
import {etemplate2} from "../../api/js/etemplate/etemplate2"; import {etemplate2} from "../../api/js/etemplate/etemplate2";
import {et2_IPrint} from "../../api/js/etemplate/et2_core_interfaces"; import {et2_IPrint} from "../../api/js/etemplate/et2_core_interfaces";
import {repeat} from "lit/directives/repeat.js";
/** /**
* @summary Application component inside EgwFramework * @summary Application component inside EgwFramework
@ -92,6 +93,9 @@ export class EgwFrameworkApp extends LitElement
@property({reflect: true}) @property({reflect: true})
name = "Application name"; name = "Application name";
@property()
title : string = "Application title";
@property() @property()
url = ""; url = "";
@ -145,11 +149,14 @@ export class EgwFrameworkApp extends LitElement
{ {
this.rightPanelInfo.preferenceWidth = typeof width !== "undefined" ? parseInt(width) : this.rightPanelInfo.defaultWidth; this.rightPanelInfo.preferenceWidth = typeof width !== "undefined" ? parseInt(width) : this.rightPanelInfo.defaultWidth;
}); });
this.addEventListener("load", this.handleEtemplateLoad);
} }
disconnectedCallback() disconnectedCallback()
{ {
super.disconnectedCallback(); super.disconnectedCallback();
this.removeEventListener("load", this.handleEtemplateLoad);
} }
firstUpdated() firstUpdated()
@ -199,22 +206,26 @@ export class EgwFrameworkApp extends LitElement
return this.loadingPromise = this.egw.request( return this.loadingPromise = this.egw.request(
this.framework.getMenuaction('ajax_exec', targetUrl, this.name), this.framework.getMenuaction('ajax_exec', targetUrl, this.name),
[targetUrl] [targetUrl]
).then((data : string[]) => ).then((data : string | string[] | { DOMNodeID? : string } | { DOMNodeID? : string }[]) =>
{ {
if(!data)
{
return;
}
// Load request returns HTML. Shove it in. // Load request returns HTML. Shove it in.
if(typeof data == "string" || typeof data == "object" && typeof data[0] == "string") if(typeof data == "string" || typeof data == "object" && typeof data[0] == "string")
{ {
render(html`${unsafeHTML(data.join(""))}`, this); render(html`${unsafeHTML((<string[]>data).join(""))}`, this);
} }
else else
{ {
// We got some data, use it // We got some data, use it
if(data.DOMNodeID) const items = (Array.isArray(data) ? data : [data])
{ .filter(data => (typeof data.DOMNodeID == "string" && document.querySelector("[id='" + data.DOMNodeID + "']") == null));
this.id = data.DOMNodeID;
} render(html`${repeat(items, i => i.DOMNodeID, (item) => html`
<div id="${item.DOMNodeID}"></div>`)}`, this);
} }
this.addEventListener("load", this.handleEtemplateLoad, {once: true});
// Might have just slotted aside content, hasSlotController will requestUpdate() // Might have just slotted aside content, hasSlotController will requestUpdate()
// but we need to do it anyway for translation // but we need to do it anyway for translation
@ -367,7 +378,7 @@ export class EgwFrameworkApp extends LitElement
get egw() get egw()
{ {
return window.egw ?? (<EgwFramework>this.parentElement).egw ?? null; return window.egw(this.name) ?? (<EgwFramework>this.parentElement).egw ?? null;
} }
get framework() : EgwFramework get framework() : EgwFramework
@ -375,6 +386,11 @@ export class EgwFrameworkApp extends LitElement
return this.closest("egw-framework"); return this.closest("egw-framework");
} }
get appName() : string
{
return this.name;
}
private hasSideContent(side : "left" | "right") private hasSideContent(side : "left" | "right")
{ {
return this.hasSlotController.test(`${side}-header`) || return this.hasSlotController.test(`${side}-header`) ||
@ -387,8 +403,8 @@ export class EgwFrameworkApp extends LitElement
*/ */
protected handleEtemplateLoad(event) protected handleEtemplateLoad(event)
{ {
const etemplate = etemplate2.getById(this.id); const etemplate = etemplate2.getById(event.target.id);
if(!etemplate) if(!etemplate || !event.composedPath().includes(this))
{ {
return; return;
} }
@ -557,7 +573,7 @@ export class EgwFrameworkApp extends LitElement
></sl-icon-button>` ></sl-icon-button>`
: nothing : nothing
} }
<h2>${this.egw?.lang(this.name) ?? this.name}</h2> <h2>${this.title ?? this.egw?.lang(this.name) ?? this.name}</h2>
</div> </div>
<header class="egw_fw_app__header" part="header"> <header class="egw_fw_app__header" part="header">
<slot name="main-header"><span class="placeholder"> ${this.name} main-header</span></slot> <slot name="main-header"><span class="placeholder"> ${this.name} main-header</span></slot>

View File

@ -23,12 +23,4 @@ document.addEventListener('DOMContentLoaded', () =>
{ {
window.egw.open_link(e.detail.item.value); 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);
});
}); });