mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-12-28 01:29:05 +01:00
Et2LinkTo: Start of link-to
still needs to work for new entries
This commit is contained in:
parent
ef0a549d1c
commit
b58045c19f
@ -81,6 +81,7 @@ export class Et2LinkAppSelect extends SlotMixin(Et2Select)
|
||||
constructor()
|
||||
{
|
||||
super();
|
||||
this.only_app = "";
|
||||
this.app_icons = true;
|
||||
this.application_list = [];
|
||||
this.hoist = true;
|
||||
@ -93,13 +94,17 @@ export class Et2LinkAppSelect extends SlotMixin(Et2Select)
|
||||
|
||||
set only_app(app : string)
|
||||
{
|
||||
this.__only_app = app;
|
||||
this.style.display = app ? 'inline' : 'none';
|
||||
this.__only_app = app || "";
|
||||
this.updateComplete.then(() =>
|
||||
{
|
||||
this.style.display = this.only_app ? 'none' : '';
|
||||
});
|
||||
}
|
||||
|
||||
get only_app() : string
|
||||
{
|
||||
return this.__only_app;
|
||||
// __only_app may be undefined during creation
|
||||
return this.__only_app || "";
|
||||
}
|
||||
|
||||
connectedCallback()
|
||||
@ -154,7 +159,7 @@ export class Et2LinkAppSelect extends SlotMixin(Et2Select)
|
||||
|
||||
get value()
|
||||
{
|
||||
return this.__only_app ? this.__only_app : super.value;
|
||||
return this.only_app ? this.only_app : super.value;
|
||||
}
|
||||
|
||||
set value(new_value)
|
||||
@ -221,7 +226,7 @@ export class Et2LinkAppSelect extends SlotMixin(Et2Select)
|
||||
|
||||
_iconTemplate(appname)
|
||||
{
|
||||
let url = this.egw().image('navbar', appname);
|
||||
let url = appname ? this.egw().image('navbar', appname) : "";
|
||||
return html`
|
||||
<et2-image style="width: var(--icon-width)" slot="prefix" src="${url}"></et2-image>`;
|
||||
}
|
||||
|
@ -14,13 +14,6 @@ import {FormControlMixin, ValidateMixin} from "@lion/form-core";
|
||||
import {Et2LinkSearch} from "./Et2LinkSearch";
|
||||
import {LinkInfo} from "./Et2Link";
|
||||
|
||||
export interface LinkEntry
|
||||
{
|
||||
app : string;
|
||||
id : string | number;
|
||||
title? : string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
else if(typeof this._value !== "undefined")
|
||||
else if(typeof this._value !== "undefined" && this._value.app)
|
||||
{
|
||||
app.value = this._value.app;
|
||||
}
|
||||
@ -94,7 +87,7 @@ export class Et2LinkEntry extends Et2InputWidget(FormControlMixin(ValidateMixin(
|
||||
select: () =>
|
||||
{
|
||||
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)
|
||||
{
|
||||
@ -150,8 +143,11 @@ export class Et2LinkEntry extends Et2InputWidget(FormControlMixin(ValidateMixin(
|
||||
|
||||
set only_app(app)
|
||||
{
|
||||
this.__only_app = app;
|
||||
this.app = app;
|
||||
this.__only_app = app || "";
|
||||
if(app)
|
||||
{
|
||||
this.app = app;
|
||||
}
|
||||
}
|
||||
|
||||
get only_app()
|
||||
@ -170,7 +166,7 @@ export class Et2LinkEntry extends Et2InputWidget(FormControlMixin(ValidateMixin(
|
||||
|
||||
get app()
|
||||
{
|
||||
return this._appNode?.value || this.__app;
|
||||
return this._appNode?.value || "";
|
||||
}
|
||||
|
||||
get _appNode() : Et2LinkAppSelect
|
||||
@ -187,8 +183,8 @@ export class Et2LinkEntry extends Et2InputWidget(FormControlMixin(ValidateMixin(
|
||||
protected _bindListeners()
|
||||
{
|
||||
this._appNode.addEventListener("change", this._handleAppChange);
|
||||
this.addEventListener("sl-select", this._handleEntrySelect);
|
||||
this.addEventListener("sl-clear", this._handleEntryClear);
|
||||
this._searchNode.addEventListener("sl-select", this._handleEntrySelect);
|
||||
this._searchNode.addEventListener("sl-clear", this._handleEntryClear);
|
||||
this.addEventListener("sl-show", this._handleShow);
|
||||
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)
|
||||
{
|
||||
@ -276,11 +272,11 @@ export class Et2LinkEntry extends Et2InputWidget(FormControlMixin(ValidateMixin(
|
||||
} : this._value;
|
||||
}
|
||||
|
||||
set value(val: LinkEntry|string|number)
|
||||
set value(val : LinkInfo | string | number)
|
||||
{
|
||||
let value : LinkInfo = {app: "", id: ""};
|
||||
|
||||
if(typeof val === 'string')
|
||||
if(typeof val === 'string' && val.length > 0)
|
||||
{
|
||||
if(val.indexOf(',') > 0)
|
||||
{
|
||||
@ -290,11 +286,11 @@ export class Et2LinkEntry extends Et2InputWidget(FormControlMixin(ValidateMixin(
|
||||
value.app = vals[0];
|
||||
value.id = vals[1];
|
||||
}
|
||||
else if(typeof val === "number")
|
||||
else if(typeof val === "number" && 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);
|
||||
}
|
||||
|
473
api/js/etemplate/Et2Link/Et2LinkTo.ts
Normal file
473
api/js/etemplate/Et2Link/Et2LinkTo.ts
Normal 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, '"') + "'] > 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);
|
@ -48,6 +48,7 @@ import './Et2Link/Et2LinkEntry';
|
||||
import './Et2Link/Et2LinkList';
|
||||
import './Et2Link/Et2LinkSearch';
|
||||
import './Et2Link/Et2LinkString';
|
||||
import './Et2Link/Et2LinkTo';
|
||||
import './Et2Select/Et2Select';
|
||||
import './Et2Select/Et2SelectAccount';
|
||||
import './Et2Select/Et2SelectReadonly';
|
||||
|
@ -999,6 +999,7 @@ span.et2_file_span {
|
||||
background-size: 16px;
|
||||
cursor: pointer;
|
||||
height: 15px;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
text-indent: 22px;
|
||||
white-space: nowrap;
|
||||
|
Loading…
Reference in New Issue
Block a user