mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-11-25 01:13:25 +01:00
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:
parent
7448377e96
commit
5bc5777db1
@ -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>
|
||||
|
||||
|
||||
|
@ -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(() =>
|
||||
{
|
||||
@ -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))}
|
||||
</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
|
||||
}
|
@ -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) {
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user