Et2LinkTo: Start of link-to

still needs to work for new entries
This commit is contained in:
nathan 2022-06-06 13:31:33 -06:00
parent ef0a549d1c
commit b58045c19f
5 changed files with 500 additions and 24 deletions

View File

@ -81,6 +81,7 @@ export class Et2LinkAppSelect extends SlotMixin(Et2Select)
constructor() constructor()
{ {
super(); super();
this.only_app = "";
this.app_icons = true; this.app_icons = true;
this.application_list = []; this.application_list = [];
this.hoist = true; this.hoist = true;
@ -93,13 +94,17 @@ export class Et2LinkAppSelect extends SlotMixin(Et2Select)
set only_app(app : string) set only_app(app : string)
{ {
this.__only_app = app; this.__only_app = app || "";
this.style.display = app ? 'inline' : 'none'; this.updateComplete.then(() =>
{
this.style.display = this.only_app ? 'none' : '';
});
} }
get only_app() : string get only_app() : string
{ {
return this.__only_app; // __only_app may be undefined during creation
return this.__only_app || "";
} }
connectedCallback() connectedCallback()
@ -154,7 +159,7 @@ export class Et2LinkAppSelect extends SlotMixin(Et2Select)
get value() get value()
{ {
return this.__only_app ? this.__only_app : super.value; return this.only_app ? this.only_app : super.value;
} }
set value(new_value) set value(new_value)
@ -221,7 +226,7 @@ export class Et2LinkAppSelect extends SlotMixin(Et2Select)
_iconTemplate(appname) _iconTemplate(appname)
{ {
let url = this.egw().image('navbar', appname); let url = appname ? this.egw().image('navbar', appname) : "";
return html` return html`
<et2-image style="width: var(--icon-width)" slot="prefix" src="${url}"></et2-image>`; <et2-image style="width: var(--icon-width)" slot="prefix" src="${url}"></et2-image>`;
} }

View File

@ -14,13 +14,6 @@ import {FormControlMixin, ValidateMixin} from "@lion/form-core";
import {Et2LinkSearch} from "./Et2LinkSearch"; import {Et2LinkSearch} from "./Et2LinkSearch";
import {LinkInfo} from "./Et2Link"; import {LinkInfo} from "./Et2Link";
export interface LinkEntry
{
app : string;
id : string | number;
title? : string;
}
/** /**
* Find and select a single entry using the link system. * Find and select a single entry using the link system.
* *
@ -85,7 +78,7 @@ export class Et2LinkEntry extends Et2InputWidget(FormControlMixin(ValidateMixin(
{ {
app.only_app = this.__only_app; app.only_app = this.__only_app;
} }
else if(typeof this._value !== "undefined") else if(typeof this._value !== "undefined" && this._value.app)
{ {
app.value = this._value.app; app.value = this._value.app;
} }
@ -94,7 +87,7 @@ export class Et2LinkEntry extends Et2InputWidget(FormControlMixin(ValidateMixin(
select: () => select: () =>
{ {
const select = <Et2LinkSearch><unknown>document.createElement("et2-link-search"); const select = <Et2LinkSearch><unknown>document.createElement("et2-link-search");
if(typeof this._value !== "undefined") if(typeof this._value !== "undefined" && this._value.id)
{ {
if(this._value.title) if(this._value.title)
{ {
@ -150,8 +143,11 @@ export class Et2LinkEntry extends Et2InputWidget(FormControlMixin(ValidateMixin(
set only_app(app) set only_app(app)
{ {
this.__only_app = app; this.__only_app = app || "";
this.app = app; if(app)
{
this.app = app;
}
} }
get only_app() get only_app()
@ -170,7 +166,7 @@ export class Et2LinkEntry extends Et2InputWidget(FormControlMixin(ValidateMixin(
get app() get app()
{ {
return this._appNode?.value || this.__app; return this._appNode?.value || "";
} }
get _appNode() : Et2LinkAppSelect get _appNode() : Et2LinkAppSelect
@ -187,8 +183,8 @@ export class Et2LinkEntry extends Et2InputWidget(FormControlMixin(ValidateMixin(
protected _bindListeners() protected _bindListeners()
{ {
this._appNode.addEventListener("change", this._handleAppChange); this._appNode.addEventListener("change", this._handleAppChange);
this.addEventListener("sl-select", this._handleEntrySelect); this._searchNode.addEventListener("sl-select", this._handleEntrySelect);
this.addEventListener("sl-clear", this._handleEntryClear); this._searchNode.addEventListener("sl-clear", this._handleEntryClear);
this.addEventListener("sl-show", this._handleShow); this.addEventListener("sl-show", this._handleShow);
this.addEventListener("sl-hide", this._handleHide); this.addEventListener("sl-hide", this._handleHide);
} }
@ -263,7 +259,7 @@ export class Et2LinkEntry extends Et2InputWidget(FormControlMixin(ValidateMixin(
} }
} }
get value() : LinkEntry | string | number get value() : LinkInfo | string | number
{ {
if(this.only_app) if(this.only_app)
{ {
@ -276,11 +272,11 @@ export class Et2LinkEntry extends Et2InputWidget(FormControlMixin(ValidateMixin(
} : this._value; } : this._value;
} }
set value(val: LinkEntry|string|number) set value(val : LinkInfo | string | number)
{ {
let value : LinkInfo = {app: "", id: ""}; let value : LinkInfo = {app: "", id: ""};
if(typeof val === 'string') if(typeof val === 'string' && val.length > 0)
{ {
if(val.indexOf(',') > 0) if(val.indexOf(',') > 0)
{ {
@ -290,11 +286,11 @@ export class Et2LinkEntry extends Et2InputWidget(FormControlMixin(ValidateMixin(
value.app = vals[0]; value.app = vals[0];
value.id = vals[1]; value.id = vals[1];
} }
else if(typeof val === "number") else if(typeof val === "number" && val)
{ {
value.id = String(val); value.id = String(val);
} }
else // object with attributes: app, id, title else if(typeof val === "object") // object with attributes: app, id, title
{ {
value = (<LinkInfo>val); value = (<LinkInfo>val);
} }

View File

@ -0,0 +1,473 @@
/**
* 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 {Et2InputWidget} from "../Et2InputWidget/Et2InputWidget";
import {FormControlMixin, ValidateMixin} from "@lion/form-core";
import {css, html, LitElement, ScopedElementsMixin} from "@lion/core";
import {et2_createWidget, et2_widget} from "../et2_core_widget";
import {et2_file} from "../et2_widget_file";
import {et2_tabbox} from "../et2_widget_tabs";
import {Et2Button} from "../Et2Button/Et2Button";
import {Et2LinkEntry} from "./Et2LinkEntry";
import {egw} from "../../jsapi/egw_global";
import {et2_vfsSelect} from "../et2_widget_vfs";
import {LinkInfo} from "./Et2Link";
import {Et2LinkList} from "./Et2LinkList";
import {et2_DOMWidget} from "../et2_core_DOMWidget";
import {et2_link_list} from "../et2_widget_link";
import {ValidationType} from "@lion/form-core/types/validate/ValidateMixinTypes";
import {ManualMessage} from "../Validators/ManualMessage";
/**
* Choose an existing entry, VFS file or local file, and link it to the current entry.
*
* If there is no "current entry", link information will be stored for submission instead
* of being directly linked.
*/
export class Et2LinkTo extends Et2InputWidget(ScopedElementsMixin(FormControlMixin(ValidateMixin(LitElement))))
{
static get properties()
{
return {
...super.properties,
/**
* Hide buttons to attach files
*/
no_files: {type: Boolean},
/**
* Limit to just this application - hides app selection
*/
only_app: {type: String},
/**
* Limit to the listed applications (comma seperated)
*/
application_list: {type: String},
value: {type: Object}
}
}
static get styles()
{
return [
...super.styles,
css`
:host(.can_link) #link_button {
display: initial;
}
#link_button {
display: none;
}
et2-link-entry {
flex: 1 1 auto;
}
.input-group__container {
flex: 1 1 auto;
}
.input-group {
display: flex;
width: 100%;
}
::slotted(.et2_file) {
width: 30px;
}
`
];
}
// Still not sure what this does, but it's important.
// Seems to be related to rendering and what's available "inside"
static get scopedElements()
{
return {
// @ts-ignore
...super.scopedElements,
'et2-button': Et2Button,
'et2-link-entry': Et2LinkEntry
};
}
constructor()
{
super();
this.no_files = false;
this.handleFilesUploaded = this.handleFilesUploaded.bind(this);
this.handleEntrySelected = this.handleEntrySelected.bind(this);
this.handleLinkButtonClick = this.handleLinkButtonClick.bind(this);
}
connectedCallback()
{
super.connectedCallback();
// Add file buttons in
// TODO: Replace when they're webcomponents
this._fileButtons();
}
/**
* @return {TemplateResult}
* @protected
*/
_inputGroupInputTemplate()
{
return html`
<et2-link-entry .only_app="${this.only_app}"
.application_list="${this.application_list}"
@sl-select=${this.handleEntrySelected}>
</et2-link-entry>
<et2-button id="link_button" label="Link" class="link" .noSubmit=${true}
@click=${this.handleLinkButtonClick}>
</et2-button>
`;
}
// TODO: Replace when they're webcomponents
_fileButtons()
{
if(this.no_files)
{
return "";
}
// File upload
//@ts-ignore IDE doesn't know about Et2WidgetClass
let self : Et2WidgetClass | et2_widget = this;
let file_attrs = {
multiple: true,
id: this.id + '_file',
label: '',
// Make the whole template a drop target
drop_target: this.getInstanceManager().DOMContainer.getAttribute("id"),
readonly: this.readonly,
// Change to this tab when they drop
onStart: function(event, file_count)
{
// Find the tab widget, if there is one
let tabs = self;
do
{
tabs = tabs.getParent();
}
while(tabs != self.getRoot() && tabs.getType() != 'tabbox');
if(tabs != self.getRoot())
{
(<et2_tabbox><unknown>tabs).activateTab(self);
}
return true;
},
onFinish: function(event, file_count)
{
// Auto-link uploaded files
self.handleFilesUploaded(event);
}
};
this.file_upload = <et2_file>et2_createWidget("file", file_attrs, this);
this.file_upload.set_readonly(this.readonly);
this.file_upload.getDOMNode().slot = "before";
this.append(this.file_upload.getDOMNode());
// Filemanager select
var select_attrs : any = {
button_label: egw.lang('Link'),
button_caption: '',
button_icon: 'link',
readonly: this.readonly,
dialog_title: egw.lang('Link'),
extra_buttons: [{text: egw.lang("copy"), id: "copy", image: "copy"},
{text: egw.lang("move"), id: "move", image: "move"}],
onchange: function()
{
var values = true;
// If entry not yet saved, store for linking on server
if(!self.value.to_id || typeof self.value.to_id == 'object')
{
values = self.value.to_id || {};
var files = this.getValue();
if(typeof files !== 'undefined')
{
for(var i = 0; i < files.length; i++)
{
values['link:' + files[i]] = {
app: 'link',
id: files[i],
type: 'unknown',
icon: 'link',
remark: '',
title: files[i]
};
}
}
}
self._link_result(values);
}
};
// only set server-side callback, if we have a real application-id (not null or array)
// otherwise it only gives an error on server-side
if(self.value && self.value.to_id && typeof self.value.to_id != 'object')
{
select_attrs.method = 'EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_link_existing';
select_attrs.method_id = self.value.to_app + ':' + self.value.to_id;
}
this.vfs_select = <et2_vfsSelect>et2_createWidget("vfs-select", select_attrs, this);
this.vfs_select.set_readonly(this.readonly);
this.vfs_select.getDOMNode().slot = "before";
this.append(this.vfs_select.getDOMNode())
}
/**
* Create links
*
* Using current value for one end of the link, create links to the provided files or entries
*
* @param _links
*/
createLink(_links : LinkInfo[])
{
let links : LinkInfo[];
if(typeof _links == 'undefined')
{
links = [];
}
else
{
links = _links;
}
// If no link array was passed in, don't make the ajax call
if(links.length > 0)
{
egw.request("EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_link",
[this.value.to_app, this.value.to_id, links]).then((result) => this._link_result(result))
}
}
/**
* Sent some links, server has a result
*
* @param {Object} success
*/
_link_result(success)
{
if(success)
{
// Show some kind of success...
// Reset
this.resetAfterLink();
// Server says it's OK, but didn't store - we'll send this again on submit
// This happens if you link to something before it's saved to the DB
if(typeof success == "object")
{
// Save as appropriate in value
if(typeof this.value != "object")
{
this.value = {};
}
this.value.to_id = success;
debugger;
for(let link in success)
{
// Icon should be in registry
if(typeof success[link].icon == 'undefined')
{
success[link].icon = egw.link_get_registry(success[link].app, 'icon');
// No icon, try by mime type - different place for un-saved entries
if(success[link].icon == false && success[link].id.type)
{
// Triggers icon by mime type, not thumbnail or app
success[link].type = success[link].id.type;
success[link].icon = true;
}
}
// Special handling for file - if not existing, we can't ask for title
if(success[link].app == 'file' && typeof success[link].title == 'undefined')
{
success[link].title = success[link].id.name || '';
}
}
}
// Look for a link-list with the same ID, refresh it
var self = this;
var list_widget = null;
this.getRoot().iterateOver(
function(widget)
{
if(widget.id == self.id)
{
list_widget = widget;
if(success === true)
{
widget._get_links();
}
}
},
this, et2_link_list
);
// If there's an array of data (entry is not yet saved), updating the list will
// not work, so add them in explicitly.
if(list_widget && success)
{
// Clear list
list_widget.set_value(null);
// Add temp links in
for(var link_id in success)
{
let link = success[link_id];
if(typeof link.title == 'undefined')
{
// Callback to server for title
egw.link_title(link.app, link.id, true).then(title =>
{
link.title = title;
list_widget._add_link(link);
});
}
else
{
// Add direct
list_widget._add_link(link);
}
}
}
// Update any neighbouring link lists
(<Et2LinkList><unknown>(<et2_DOMWidget>this.getParent()).getDOMNode().querySelector('et2-link-list'))?.get_links(Object.values(success));
}
else
{
this.validators.push(new ManualMessage(success));
}
this.dispatchEvent(new CustomEvent('link.et2_link_to', {bubbles: true, detail: success}));
}
/**
* A link was attempted. Reset internal values to get ready for the next one.
*/
resetAfterLink()
{
// Hide link button again
this.classList.remove("can_link");
this.link_button.image = "";
// Clear internal
delete this.value.app;
delete this.value.id;
// Clear file upload
for(var file in this.file_upload.options.value)
{
delete this.file_upload.options.value[file];
}
this.file_upload.progress.empty();
// Clear link entry
this.select.value = {app: this.select.app, id: ""};
}
/**
* Files have been uploaded (successfully), ready to link
*
* @param event
* @protected
*/
handleFilesUploaded(event)
{
this.classList.add("can_link");
let links = [];
// Get files from file upload widget
let files = this.file_upload.get_value();
for(let file in files)
{
links.push({
app: 'file',
id: file,
name: files[file].name,
type: files[file].type,
// Not sure what this is...
/*
remark: jQuery("li[file='" + files[file].name.replace(/'/g, '&quot') + "'] > input", self.file_upload.progress)
.filter(function ()
{
return jQuery(this).attr("placeholder") != jQuery(this).val();
}).val()
*/
});
}
this.createLink(links);
}
/**
* An entry has been selected, ready to link
*
*/
handleEntrySelected(event)
{
// Could be the app, could be they selected an entry
if(event.currentTarget == this.select)
{
this.classList.add("can_link");
}
}
handleLinkButtonClick(event : MouseEvent)
{
this.link_button.image = "loading";
let link_info : LinkInfo[] = [];
if(this.select.value)
{
let selected = this.select.value;
// Extra complicated because LinkEntry doesn't always return a LinkInfo
if(this.only_app)
{
selected = <LinkInfo>{app: this.only_app, id: selected};
}
link_info.push(<LinkInfo>selected);
}
this.createLink(link_info)
}
get link_button() : Et2Button
{
return this.shadowRoot.querySelector("#link_button");
}
get select() : Et2LinkEntry
{
return this.shadowRoot.querySelector("et2-link-entry");
}
/**
* Types of validation supported by this FormControl (for instance 'error'|'warning'|'info')
*
* @type {ValidationType[]}
*/
static get validationTypes() : ValidationType[]
{
return ['error', 'success'];
}
}
// @ts-ignore TypeScript is not recognizing that this widget is a LitElement
customElements.define("et2-link-to", Et2LinkTo);

View File

@ -48,6 +48,7 @@ import './Et2Link/Et2LinkEntry';
import './Et2Link/Et2LinkList'; import './Et2Link/Et2LinkList';
import './Et2Link/Et2LinkSearch'; import './Et2Link/Et2LinkSearch';
import './Et2Link/Et2LinkString'; import './Et2Link/Et2LinkString';
import './Et2Link/Et2LinkTo';
import './Et2Select/Et2Select'; import './Et2Select/Et2Select';
import './Et2Select/Et2SelectAccount'; import './Et2Select/Et2SelectAccount';
import './Et2Select/Et2SelectReadonly'; import './Et2Select/Et2SelectReadonly';

View File

@ -999,6 +999,7 @@ span.et2_file_span {
background-size: 16px; background-size: 16px;
cursor: pointer; cursor: pointer;
height: 15px; height: 15px;
width: 100%;
text-align: left; text-align: left;
text-indent: 22px; text-indent: 22px;
white-space: nowrap; white-space: nowrap;