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>
<!-- 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>

View File

@ -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 ?? <typeof 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 = <EgwFrameworkApp>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(() =>
{
@ -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))}
</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) {
.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) {

View File

@ -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((<string[]>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`
<div id="${item.DOMNodeID}"></div>`)}`, 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 ?? (<EgwFramework>this.parentElement).egw ?? null;
return window.egw(this.name) ?? (<EgwFramework>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
></sl-icon-button>`
: nothing
}
<h2>${this.egw?.lang(this.name) ?? this.name}</h2>
<h2>${this.title ?? this.egw?.lang(this.name) ?? this.name}</h2>
</div>
<header class="egw_fw_app__header" part="header">
<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);
});
/* 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);
});
});