/** * 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, PropertyValues, render, TemplateResult} from "lit"; import {until} from "lit/directives/until.js"; 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; } ::slotted(*) { display: inline; } ::slotted(*):hover { text-decoration: underline; } /* 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 */ entryId: { type: String, reflect: true }, /** * Application filter * Set to an appname or comma separated list of applications to show only linked entries from those * applications */ onlyApp: { type: String }, /** * Type filter * Sub-type key to list only entries of that type */ linkType: { type: String }, // Show links that are marked as deleted, being held for purge showDeleted: {type: Boolean}, /** * Pass value as an object, will be parsed to set application & entryId */ value: { type: Object, reflect: false } } } protected _link_list : LinkInfo[]; protected _loadingPromise : Promise; constructor() { super(); this._link_list = [] this.__showDeleted = false; } async getUpdateComplete() { const result = await super.getUpdateComplete(); if(this._loadingPromise) { // Wait for the values to arrive before we say we're done await this._loadingPromise; } return result; } /** * 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: , to_id: } List of linked entries will be fetched from the server * - Array: {app: , 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; } // We have app & ID - fetch list if(typeof _value == 'object' && !Array.isArray(_value) && _value.to_app && _value.to_id && ( typeof _value.to_id === "string" || typeof _value.to_id == "number")) { this.application = _value.to_app; this.entryId = _value.to_id; this.get_links(); return; } // CSV list of IDs for one app if(typeof _value === "string") { let ids = _value.split(","); ids.forEach((id) => (this._link_list).push({app: this.application, id: id})); } // List of LinkInfo else if(Array.isArray(_value)) { this._link_list = _value; } // List of LinkInfo stuffed into to_id - entry is not yet saved else if(_value.to_id && typeof _value.to_id !== "string") { this.entryId = _value.to_id; Object.keys(_value.to_id).forEach((key) => { this._link_list.push(_value.to_id[key]); }); } this._addLinks(this._link_list); super.requestUpdate(); } public updated(changedProperties : PropertyValues) { super.updated(changedProperties); if((changedProperties.has("application") || changedProperties.has("entryId") || changedProperties.has("onlyApp") || changedProperties.has("linkType")) && this.application && this.entryId ) { // Something changed, and we have the information needed to get the matching links this.get_links(); } } 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` `; } /** * Render one link * * @param link * @returns {TemplateResult} * @protected */ protected _linkTemplate(link : LinkInfo) : TemplateResult { const id = typeof link.id === "string" ? link.id : link.link_id; return html` `; } /** * Render that we're waiting for data * @returns {TemplateResult} * @protected */ protected _loadingTemplate() : TemplateResult { return html`loading...`; } /** * Render a list of links inside the list * These get slotted, rather than put inside the shadow dom * * @param links * @protected */ protected _addLinks(links : LinkInfo[]) { // Remove anything there right now while(this.lastChild) { this.removeChild(this.lastChild); } links.forEach((link) => { let temp = document.createElement("div"); render(this._linkTemplate(link), temp); temp.childNodes.forEach((node) => this.appendChild(node)); }) /* This should work, and it does, but only once. It fails if you try and update then run it again - none of the children get added Something about how lit renders render(html`${repeat(links, (link) => link.app + ":" + link.id, (link) => this._linkTemplate(link))}`, this ); */ this.dispatchEvent(new Event("change", {bubbles: true})); } /** * Starts the request for link list to the server * * Called internally to fetch the list. May be called externally to trigger a refresh if a link is added. * */ public get_links(not_saved_links? : LinkInfo[]) { if(typeof not_saved_links === "undefined") { not_saved_links = []; } let _value = { to_app: this.application, to_id: this.entryId, only_app: this.onlyApp, show_deleted: this.showDeleted }; if(this._loadingPromise) { // Already waiting return; } this._loadingPromise = >(this.egw().jsonq('EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_link_list', [_value])) .then(_value => { if(_value && Array.isArray(_value)) { for(let link of _value) { if(!not_saved_links.some(l => l.app == link.app && l.id == link.id)) { not_saved_links.push(link); } } } this._addLinks(not_saved_links); this._loadingPromise = null; }) } getDetachedAttributes(_attrs : string[]) { _attrs.push("application", "entryId", "statustext"); } getDetachedNodes() : HTMLElement[] { return [this]; } setDetachedAttributes(_nodes : HTMLElement[], _values : object, _data?) { for(let k in _values) { this[k] = _values[k]; } } }; customElements.define("et2-link-string", Et2LinkString);