WIP on Et2Link & Et2LinkString

This commit is contained in:
nathan 2022-05-09 16:06:16 -06:00
parent 1fb46bb74c
commit 07ced8046d
2 changed files with 522 additions and 0 deletions

View File

@ -0,0 +1,288 @@
/**
* EGroupware eTemplate2 - JS Link object
*
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
* @package etemplate
* @subpackage api
* @link https://www.egroupware.org
* @author Nathan Gray
* @copyright 2022 Nathan Gray
*/
import {ExposeMixin} from "../Expose/ExposeMixin";
import {css, html, LitElement} from "@lion/core";
import {Et2Widget} from "../Et2Widget/Et2Widget";
import {et2_IDetachedDOM} from "../et2_core_interfaces";
/**
* Display a specific, single entry from an application
*
* The entry is specified with the application name, and the app's ID for that entry.
* You can set it directly in the properties (application, entry_id) or use set_value() to
* pass an object {app: string, id: string, [title: string]} or string in the form <application>::<ID>.
* If title is not specified, it will be fetched using framework's egw.link_title()
*/
// @ts-ignore TypeScript says there's something wrong with types
export class Et2Link extends ExposeMixin<Et2Widget>(Et2Widget(LitElement)) implements et2_IDetachedDOM
{
static get styles()
{
return [
...super.styles,
css`
:host {
display: block;
}
/** Style based on parent **/
:host-context(et2-link-string) {
display: inline;
}
:host-context(et2-link-list) {
background-color: green;
}
`
];
}
static get properties()
{
return {
...super.properties,
/**
* Specify the application for the entry
*/
app: {
type: String,
reflect: true,
},
/**
* Application entry ID
*/
entry_id: {
type: String,
reflect: true
},
/**
* Pass value as an object, will be parsed to set application & entry_id
*/
value: {
type: Object,
reflect: false
},
/**
* View link type
* Used for displaying the linked entry
* [view|edit|add]
* default "view"
*/
link_hook: {
type: String
},
/**
* Target application
*
* Passed to egw.open() to open entry in specified application
*/
target_app: {
type: String
},
/**
* Optional parameter to be passed to egw().open in order to open links in specified target eg. _blank
*/
extra_link_target: {
type: String
},
/**
* Breaks title into multiple lines based on this delimiter by replacing it with '\r\n'"
*/
break_title: {
type: String
}
}
}
private static MISSING_TITLE = "??";
// Title is read-only inside
private _title : string;
private _titlePromise : Promise<string>;
constructor()
{
super();
this._title = "";
this.__link_hook = "view";
}
connectedCallback()
{
super.connectedCallback();
this.classList.add(...["et2_clickable", "et2_link"]);
}
render()
{
let title = this.title;
if(this.break_title)
{
// Set up title to optionally break on the provided character - replace all space with nbsp, add a
// zero-width space after the break string
title = title
.replace(this.break_title, this.break_title.trimEnd() + "\u200B")
.replace(/ /g, '\u00a0');
}
return html`${title}`;
}
public set title(_title)
{
this._title = _title;
}
public get title()
{
return this._title;
}
set value(_value : LinkInfo | string)
{
if(!_value)
{
this.app = "";
this.id = "";
this.title = "";
return;
}
if(typeof _value != 'object' && _value)
{
if(_value.indexOf(':') >= 0)
{
// application_name:ID
let app = _value.split(':', 1);
let id = _value.substr(app[0].length + 1);
_value = {app: app[0], id: id};
}
else if(this.app)
{
// Application set, just passed ID
_value = {app: this.app, id: _value};
}
else
{
console.warn("Bad value for link widget. Need an object with keys 'app', 'id', and optionally 'title'", _value);
return;
}
}
if(typeof _value !== "string")
{
this.app = _value.app;
this.entry_id = _value.id;
this._title = Et2Link.MISSING_TITLE;
if(_value.title)
{
this._title = _value.title;
}
Object.keys(_value).forEach(key =>
{
if(["app", "entry_id", "title", "id"].indexOf(key) != -1)
{
return;
}
this.dataset[key] = _value[key];
})
}
}
set_value(_value : LinkInfo | string)
{
this.value = _value;
}
/**
* If app or entry_id has changed, we'll update the title
*
* @param changedProperties
*/
willUpdate(changedProperties)
{
super.willUpdate(changedProperties);
super.requestUpdate();
if(changedProperties.has("app") || changedProperties.has("entry_id"))
{
if(!this.app || !this.entry_id || (this.app && this.entry_id && !this._title))
{
this._title = Et2Link.MISSING_TITLE;
}
if(this.app && this.entry_id && this._title == Et2Link.MISSING_TITLE)
{
// Title will be fetched from server and then set
this._titlePromise = this.egw()?.link_title(this.app, this.entry_id, true).then(title =>
{
this._title = title;
// It's probably already been rendered
this.requestUpdate();
});
}
}
}
_handleClick(_ev : MouseEvent) : boolean
{
this.egw().open(Object.assign({
app: this.app,
id: this.entry_id
}, this.dataset), "", this.link_hook, this.dataset.extra_args, this.target_app || this.app, this.target_app);
_ev.stopImmediatePropagation();
return true;
}
getDetachedAttributes(_attrs : string[])
{
_attrs.push("app", "entry_id");
}
getDetachedNodes() : HTMLElement[]
{
return [<HTMLElement><unknown>this];
}
setDetachedAttributes(_nodes : HTMLElement[], _values : object, _data?)
{
for(let k in _values)
{
this[k] = _values[k];
}
}
}
// @ts-ignore TypeScript says there's something wrong with types
customElements.define("et2-link", Et2Link);
/**
* Interface to describe needed information about a link
*/
export interface LinkInfo
{
app : string,
id : string,
title? : string,
comment? : string
icon? : string,
// Extra information for things like files
download_url? : string,
target? : string,
mode? : number
}

View File

@ -0,0 +1,234 @@
/**
* EGroupware eTemplate2 - JS Link list object
*
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
* @package etemplate
* @subpackage api
* @link https://www.egroupware.org
* @author Nathan Gray
* @copyright 2022 Nathan Gray
*/
import {css, html, LitElement, render, repeat, TemplateResult, until} from "@lion/core";
import {Et2Widget} from "../Et2Widget/Et2Widget";
import {Et2Link, LinkInfo} from "./Et2Link";
import {et2_IDetachedDOM} from "../et2_core_interfaces";
/**
* Display a list of entries in a comma separated list
*
* Given an application & entry ID, will query the list of links and display
*
* @see Et2Link
*/
// @ts-ignore TypeScript says there's something wrong with types
export class Et2LinkString extends Et2Widget(LitElement) implements et2_IDetachedDOM
{
static get styles()
{
return [
...super.styles,
css`
:host {
list-style-type: none;
display: inline;
padding: 0px;
}
/* CSS for child elements */
::slotted(*):after {
content: ", "
}
::slotted(*:last-child):after {
content:initial;
}
`
];
}
static get properties()
{
return {
...super.properties,
/**
* Specify the application for all the entries, so you only need to specify the entry ID
*/
application: {
type: String,
reflect: true,
},
/**
* Application entry ID
*/
entry_id: {
type: String,
reflect: true
},
/**
* Application filter
* Set to an appname or comma separated list of applications to show only linked entries from those
* applications
*/
only_app: {
type: String
},
/**
* Type filter
* Sub-type key to list only entries of that type
*/
link_type: {
type: String
},
/**
* Pass value as an object, will be parsed to set application & entry_id
*/
value: {
type: Object,
reflect: false
}
}
}
protected _link_list : LinkInfo[];
protected _loadingPromise : Promise<LinkInfo[]>;
constructor()
{
super();
this._link_list = []
}
/**
* Set the value of the list
*
* Value can be:
* - String: CSV list of entries in either app:ID or just ID if application is set.
* - Object: {to_app: <appname>, to_id: <Entry ID>} List of linked entries will be fetched from the server
* - Array: {app: <appname>, id: <ID>}[]
* @param _value
*/
public set_value(_value : string | { to_app : string, to_id : string } | LinkInfo[])
{
this._link_list = [];
if(typeof _value == "object" && !Array.isArray(_value) && !_value.to_app && this.application)
{
_value.to_app = this.application;
}
if(typeof _value == 'object' && !Array.isArray(_value) && _value.to_app && _value.to_id)
{
this.application = _value.to_app;
this.entry_id = _value.to_id;
this._get_links();
return;
}
if(typeof _value === "string")
{
let ids = _value.split(",");
ids.forEach((id) => (<LinkInfo[]>this._link_list).push(<LinkInfo>{app: this.application, id: id}));
}
else if(Array.isArray(_value))
{
this._link_list = _value;
}
this._add_links(this._link_list);
super.requestUpdate();
}
public render() : TemplateResult
{
// This shows loading template until loadingPromise resolves, then shows _listTemplate
return html`
${this._loadingPromise ? until(
this._loadingPromise?.then(res =>
{
this._listTemplate();
}),
this._loadingTemplate()
) : this._listTemplate()}
`;
}
protected _listTemplate()
{
return html`
<slot></slot>`;
}
/**
* Render one link
*
* @param link
* @returns {TemplateResult}
* @protected
*/
protected _linkTemplate(link) : TemplateResult
{
return html`
<et2-link app="${link.app}" entry_id="${link.id}" .value=${link}></et2-link>`;
}
/**
* Render that we're waiting for data
* @returns {TemplateResult}
* @protected
*/
protected _loadingTemplate() : TemplateResult
{
return html`loading...`;
}
/**
* Render a list of links inside the list
* @param links
* @protected
*/
protected _add_links(links : LinkInfo[])
{
render(html`${repeat(this._link_list, (link) => link.app + ":" + link.id, (link) => this._linkTemplate(link))}`, this);
}
/**
* Starts the request for link list to the server
* @protected
*/
protected _get_links()
{
let _value = {
to_app: this.application,
to_id: this.entry_id,
only_app: this.only_app
};
this.egw().jsonq('EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_link_list', [_value]).then(_value =>
{
this._addLinks(_value);
});
return;
}
getDetachedAttributes(_attrs : string[])
{
_attrs.push("application", "entry_id");
}
getDetachedNodes() : HTMLElement[]
{
return [<HTMLElement><unknown>this];
}
setDetachedAttributes(_nodes : HTMLElement[], _values : object, _data?)
{
for(let k in _values)
{
this[k] = _values[k];
}
}
};
// @ts-ignore TypeScript says there's something wrong with types
customElements.define("et2-link-string", Et2LinkString, {extends: 'ul'});