mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-11-22 07:53:39 +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>
|
<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>
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
}
|
@ -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) {
|
||||||
|
@ -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>
|
||||||
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
});
|
Loading…
Reference in New Issue
Block a user