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}