Kdots: Implement messages in framework

This commit is contained in:
nathan 2024-07-09 11:43:45 -06:00
parent 8390b82b71
commit 4212cbc5b6
5 changed files with 314 additions and 2 deletions

View File

@ -5,6 +5,21 @@ body {
overflow: clip; overflow: clip;
padding: 0px; padding: 0px;
margin: 0px; margin: 0px;
/** Messages **/
/** end messages **/
}
html .sl-toast-stack,
body .sl-toast-stack {
top: auto;
bottom: 0px;
}
html .sl-toast-stack sl-alert et2-checkbox,
body .sl-toast-stack sl-alert et2-checkbox {
padding-top: var(--sl-spacing-large);
}
html #egw_message,
body #egw_message {
display: none;
} }
.egw_menu::part(popup) { .egw_menu::part(popup) {
z-index: var(--sl-z-index-dropdown); z-index: var(--sl-z-index-dropdown);

View File

@ -4,6 +4,23 @@ html, body {
overflow: clip; overflow: clip;
padding: 0px; padding: 0px;
margin: 0px; margin: 0px;
/** Messages **/
.sl-toast-stack {
top: auto;
bottom: 0px;
sl-alert et2-checkbox {
padding-top: var(--sl-spacing-large);
}
}
#egw_message {
display: none
}
/** end messages **/
} }
.egw_menu::part(popup) .egw_menu::part(popup)

View File

@ -3,12 +3,13 @@ import {customElement} from "lit/decorators/custom-element.js";
import {property} from "lit/decorators/property.js"; import {property} from "lit/decorators/property.js";
import {classMap} from "lit/directives/class-map.js"; import {classMap} from "lit/directives/class-map.js";
import {repeat} from "lit/directives/repeat.js"; import {repeat} from "lit/directives/repeat.js";
import {until} from "lit/directives/until.js";
import "@shoelace-style/shoelace/dist/components/split-panel/split-panel.js"; import "@shoelace-style/shoelace/dist/components/split-panel/split-panel.js";
import styles from "./EgwFramework.styles"; import styles from "./EgwFramework.styles";
import {egw} from "../../api/js/jsapi/egw_global"; import {egw} from "../../api/js/jsapi/egw_global";
import {SlDropdown, SlTab, SlTabGroup} from "@shoelace-style/shoelace"; import {SlAlert, SlDropdown, SlTab, SlTabGroup} from "@shoelace-style/shoelace";
import {EgwFrameworkApp} from "./EgwFrameworkApp"; import {EgwFrameworkApp} from "./EgwFrameworkApp";
import {until} from "lit/directives/until.js"; import {EgwFrameworkMessage} from "./EgwFrameworkMessage";
/** /**
* @summary Accessable, webComponent-based EGroupware framework * @summary Accessable, webComponent-based EGroupware framework
@ -105,6 +106,9 @@ export class EgwFramework extends LitElement
// Keep track of open popups // Keep track of open popups
private _popups : Window[] = []; private _popups : Window[] = [];
// Keep track of open messages
private _messages : SlAlert[] = [];
private get tabs() : SlTabGroup { return this.shadowRoot.querySelector("sl-tab-group");} private get tabs() : SlTabGroup { return this.shadowRoot.querySelector("sl-tab-group");}
connectedCallback() connectedCallback()
@ -143,6 +147,18 @@ export class EgwFramework extends LitElement
// These need egw fully loaded // These need egw fully loaded
this.getEgwComplete().then(() => this.getEgwComplete().then(() =>
{ {
// Regisert the "message" plugin
this.egw.registerJSONPlugin((type, res, req) =>
{
//Check whether all needed parameters have been passed and call the alertHandler function
if((typeof res.data.message != 'undefined'))
{
this.message(res.data.message, res.data.type)
return true;
}
throw 'Invalid parameters';
}, null, 'message');
// Quick add // Quick add
this.egw.link_quick_add('topmenu_info_quick_add'); this.egw.link_quick_add('topmenu_info_quick_add');
@ -493,6 +509,67 @@ export class EgwFramework extends LitElement
app.setSidebox(sideboxData, hash); app.setSidebox(sideboxData, hash);
} }
/**
* Show a message, with an optional type
*
* @param {string} message
* @param {"" | "help" | "info" | "error" | "warning" | "success"} type
* @param {number} duration The length of time, seconds, the alert will show before closing itself. Success
* messages are shown for 5s, other messages require manual closing by the user.
* @param {boolean} closable=true Message can be closed by the user
* @param {string} _discardID unique string id (appname:id) in order to register
* the message as discardable. Discardable messages offer a checkbox to never be shown again.
* If no appname given, the id will be prefixed with current app. The discardID will be stored in local storage.
* @returns {Promise<SlAlert>} SlAlert element
*/
public async message(message : string, type : "" | "help" | "info" | "error" | "warning" | "success" = "", duration : null | number = null, closable = true, _discardID : null | string = null)
{
if(message && !type)
{
const error_reg_exp = new RegExp('(error|' + egw.lang('error') + ')', 'i');
type = message.match(error_reg_exp) ? 'error' : 'success';
}
// Do not add a same message twice if it's still not dismissed
const hash = await this.egw.hashString(message);
if(typeof this._messages[hash] !== "undefined")
{
return this._messages[hash];
}
// Already discarded, just stop
if(_discardID && EgwFrameworkMessage.isDiscarded(_discardID))
{
return;
}
const attributes = {
type: type,
closable: closable,
duration: duration * 1000,
discard: _discardID,
message: message,
"data-hash": hash
}
if(!duration)
{
delete attributes.duration;
}
const alert = Object.assign(document.createElement("egw-message"), attributes);
alert.addEventListener("sl-hide", (e) =>
{
delete this._messages[e.target["data-hash"] ?? ""];
});
this._messages[hash] = alert;
document.body.append(alert);
await alert.updateComplete;
alert.toast();
return alert;
}
protected getBaseUrl() {return "";} protected getBaseUrl() {return "";}
/** /**

View File

@ -158,6 +158,7 @@ export default css`
.egw_fw_app__loading { .egw_fw_app__loading {
text-align: center; text-align: center;
margin: auto;
sl-spinner { sl-spinner {
--track-width: 1rem; --track-width: 1rem;

View File

@ -0,0 +1,202 @@
import {html, LitElement, nothing, TemplateResult} from "lit";
import {customElement} from "lit/decorators/custom-element.js";
import {property} from "lit/decorators/property.js";
import {SlAlert} from "@shoelace-style/shoelace";
import {egw} from "../../api/js/jsapi/egw_global";
import {Et2Checkbox} from "../../api/js/etemplate/Et2Checkbox/Et2Checkbox";
/**
* @summary System message
*
* @dependency sl-alert
* @dependency sl-icon
*
* @slot - Content
* @slot icon - An icon to show in the message
*
* @csspart base - Wraps it all.
* @csspart icon -
*/
@customElement('egw-message')
export class EgwFrameworkMessage extends LitElement
{
/**
* The type of message
* @type { "help" | "info" | "error" | "warning" | "success"}
*/
@property()
type : "help" | "info" | "error" | "warning" | "success" = "success";
/**
* Message
*
* @type {string}
*/
@property()
message : string = "";
/**
* Enables a close button that allows the user to dismiss the message
* @type {boolean}
*/
@property()
closable = true;
/**
* Length of time, in seconds, before the message closes automatically.
* Success messages close in 5s, for other types the default is never close.
* @type {number}
*/
@property()
duration : number = null;
/**
* Unique string id (appname:id) in order to register the message as discardable.
* Discardable messages offer a checkbox to never be shown again.
* If no appname given, the id will be prefixed with current app. The discardID will be stored in local storage.
*
* @type {string}
*/
@property()
discard : string = "";
// Map types to icons
private static ICON_MAP = {
help: "question-circle",
error: "exclamation-octagon",
warning: "exclamation-triangle",
success: "check2-circle",
info: "info-circle",
};
// Map our message types to shoelace variants
private static TYPE_MAP = {info: "primary", error: "danger"};
private __alert : SlAlert;
/**
* Check if a message has been discarded
* @param {string} discardId
* @returns {boolean}
*/
public static isDiscarded(discardId : string) : boolean
{
let discardAppName = "";
if(discardId)
{
let discardID = discardId.split(':');
if(discardID.length < 2)
{
discardId = window.egw.app_name() + ":" + discardID.pop();
}
discardAppName = discardID.length > 1 ? discardID[0] : window.egw.app_name();
}
const discarded = JSON.parse(window.egw.getLocalStorageItem(discardAppName, 'discardedMsgs'));
if(Array.isArray(discarded))
{
return discarded.includes(discardId);
}
return false;
};
/**
* Display the alert as a toast notification
* The returned promise will resolve after the message is hidden.
*
* @returns {Promise<void>}
*/
public toast() : Promise<void>
{
this.__alert = this.alert;
this.updateComplete.then(() =>
{
this.remove();
})
return this.alert.toast();
}
/**
* Show the message
* @returns {() => Promise<void>}
*/
public show() : () => Promise<void>
{
return this.alert.show;
}
/**
* Hide the message
* @returns {Promise<void>}
*/
public hide() : Promise<void>
{
return this.alert.hide();
}
get alert() : SlAlert { return this.shadowRoot?.querySelector("sl-alert") as SlAlert ?? this.__alert; }
get egw() : typeof egw
{
return window.egw
}
protected handleHide(e)
{
// Store user's discard choice, if it was offered
const check = <Et2Checkbox>this.alert.querySelector("#discard");
if(this.discard && check && check.value && !EgwFrameworkMessage.isDiscarded(this.discard))
{
const discardAppName = this.discard.split(":").shift();
let discarded : string | string[] = this.egw.getLocalStorageItem(discardAppName, 'discardedMsgs');
if(!discarded)
{
discarded = [this.discard];
}
else
{
discarded = <string[]>JSON.parse(discarded)
discarded.push(this.discard);
}
this.egw.setLocalStorageItem(discardAppName, 'discardedMsgs', JSON.stringify(discarded));
}
this.updateComplete.then(() =>
{
this.dispatchEvent(new CustomEvent("sl-hide"));
});
}
render()
{
const icon = EgwFrameworkMessage.ICON_MAP[this.type] ?? "info-circle";
const variant = EgwFrameworkMessage.TYPE_MAP[this.type] ?? "success";
const duration = this.type == "success" && !this.duration ? 5000 : this.duration;
let discard : symbol | TemplateResult = nothing;
if(this.discard && EgwFrameworkMessage.isDiscarded(this.discard))
{
// Don't show discarded messages
return nothing;
}
else if(this.discard)
{
discard = html`
<et2-checkbox
id="discard"
label="${this.egw.lang("Don't show this again")}"
></et2-checkbox>
`;
}
return html`
<sl-alert
variant=${variant}
?closable=${this.closable}
duration=${duration || nothing}
@sl-hide=${this.handleHide}
>
<sl-icon name=${icon} slot="icon"></sl-icon>
<et2-description activateLinks value=${this.message}></et2-description>
${discard}
</sl-alert>
`;
}
}