W.I.P on collabora placeholder insert

This commit is contained in:
nathan 2021-09-20 14:58:02 -06:00
parent 13198e12c9
commit 29bd739955
6 changed files with 627 additions and 5 deletions

View File

@ -0,0 +1,271 @@
/**
* EGroupware eTemplate2 - JS Placeholder widgets
*
* @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 Nathan Gray 2021
*/
/*egw:uses
et2_core_inputWidget;
et2_core_valueWidget;
et2_widget_description;
*/
import {et2_valueWidget} from "./et2_core_valueWidget";
import {et2_createWidget, et2_register_widget, WidgetConfig} from "./et2_core_widget";
import {ClassWithAttributes} from "./et2_core_inheritance";
import {et2_dialog} from "./et2_widget_dialog";
import {et2_inputWidget} from "./et2_core_inputWidget";
import type {egw} from "../jsapi/egw_global";
import {et2_selectbox} from "./et2_widget_selectbox";
import {et2_description} from "./et2_widget_description";
import {et2_link_entry} from "./et2_widget_link";
/**
* Display a dialog to choose a placeholder
*/
export class et2_placeholder_select extends et2_inputWidget
{
static readonly _attributes : any = {
insert_callback: {
"name": "Insert callback",
"description": "Method called with the selected placeholder text",
"type": "js"
}
};
static placeholders : Object | null = null;
button : JQuery;
submit_callback : any;
dialog : et2_dialog;
protected value : any;
/**
* Constructor
*
* @param _parent
* @param _attrs
* @memberOf et2_vfsSelect
*/
constructor(_parent, _attrs? : WidgetConfig, _child? : object)
{
// Call the inherited constructor
super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_placeholder_select._attributes, _child || {}));
// Allow no child widgets
this.supportedWidgetClasses = [];
}
_content(_content, _callback)
{
let self = this;
if(this.dialog && this.dialog.div)
{
this.dialog.div.dialog('close');
}
var callback = _callback || this._buildDialog;
if(et2_placeholder_select.placeholders === null)
{
this.egw().loading_prompt('placeholder_select', true, '', 'body');
this.egw().json(
'EGroupware\\Api\\Etemplate\\Widget\\Placeholder::ajax_get_placeholders',
[],
function(_content)
{
this.egw().loading_prompt('placeholder_select', false);
et2_placeholder_select.placeholders = _content;
callback.apply(self, arguments);
}.bind(this)
).sendRequest(true);
}
else
{
this._buildDialog(et2_placeholder_select.placeholders);
}
}
/**
* Builds file navigator dialog
*
* @param {object} _data content
*/
private _buildDialog(_data)
{
let self = this;
let buttons = [
{
text: this.egw().lang("Insert"),
id: "submit",
}
];
let extra_buttons_action = {};
if(this.options.extra_buttons && this.options.method)
{
for(let i = 0; i < this.options.extra_buttons.length; i++)
{
delete (this.options.extra_buttons[i]['click']);
buttons.push(this.options.extra_buttons[i]);
extra_buttons_action[this.options.extra_buttons[i]['id']] = this.options.extra_buttons[i]['id'];
}
}
buttons.push({text: this.egw().lang("Cancel"), id: "cancel", image: "cancel"});
let data = {
content: {app: '', group: ''},
sel_options: {app: [], group: []}
};
Object.keys(_data).map((key) =>
{
data.sel_options.app.push(
{
value: key,
label: this.egw().lang(key)
});
});
data.sel_options.group = this._get_group_options(Object.keys(_data)[0]);
data.content.app = data.sel_options.app[0].value;
data.content.group = data.sel_options.group[0].value;
// callback for dialog
this.submit_callback = function(submit_button_id, submit_value, savemode)
{
if((submit_button_id == 'submit' || (extra_buttons_action && extra_buttons_action[submit_button_id])) && submit_value)
{
this.options.insert_callback(submit_value.placeholder_list);
return true;
}
}.bind(this);
this.dialog = <et2_dialog>et2_createWidget("dialog",
{
callback: this.submit_callback,
title: this.options.dialog_title || this.egw().lang("Insert Placeholder"),
buttons: buttons,
minWidth: 500,
minHeight: 400,
width: 400,
value: data,
template: this.egw().webserverUrl + '/api/templates/default/insert_merge_placeholder.xet?1',
resizable: true
}, et2_dialog._create_parent('api'));
this.dialog.template.uniqueId = 'api.insert_merge_placeholder';
// Keep the dialog always at the top
this.dialog.div.parent().css({"z-index": 100000});
this.dialog.div.on('load', function(e)
{
console.log(this);
let app = <et2_selectbox>this.dialog.template.widgetContainer.getDOMWidgetById("app");
let group = <et2_selectbox>this.dialog.template.widgetContainer.getDOMWidgetById("group");
let placeholder_list = <et2_selectbox>this.dialog.template.widgetContainer.getDOMWidgetById("placeholder_list");
let preview = <et2_description>this.dialog.template.widgetContainer.getDOMWidgetById("preview_placeholder");
let entry = <et2_link_entry>this.dialog.template.widgetContainer.getDOMWidgetById("entry");
placeholder_list.set_select_options(this._get_placeholders(app.get_value(), group.get_value()));
// Further setup / styling that can't be done in etemplate
this.dialog.template.DOMContainer.style.display = "flex";
this.dialog.template.DOMContainer.firstChild.style.display = "flex";
group.getDOMNode().size = 5;
placeholder_list.getDOMNode().size = 5;
// Bind some handlers
group.onchange = (select_node, select_widget) =>
{
console.log(this, arguments);
placeholder_list.set_select_options(this._get_placeholders(app.get_value(), group.get_value()));
preview.set_value("");
}
placeholder_list.onchange = this._on_placeholder_select.bind(this);
entry.onchange = this._on_placeholder_select.bind(this);
}.bind(this));
}
doLoadingFinished()
{
this._content.call(this, null);
return true;
}
_on_placeholder_select(node, widget : et2_selectbox | et2_link_entry)
{
let app = <et2_selectbox>this.dialog.template.widgetContainer.getDOMWidgetById("app");
let entry = <et2_link_entry>this.dialog.template.widgetContainer.getDOMWidgetById("entry");
let placeholder_list = <et2_selectbox>this.dialog.template.widgetContainer.getDOMWidgetById("placeholder_list");
let preview = <et2_description>this.dialog.template.widgetContainer.getDOMWidgetById("preview_placeholder");
let preview_content = <et2_description>this.dialog.template.widgetContainer.getDOMWidgetById("preview_content");
console.log(this, arguments);
preview.set_value(placeholder_list.get_value());
if(placeholder_list.get_value() && entry.get_value() && entry.get_value().app && entry.get_value().id)
{
// Show the selected placeholder replaced with value from the selected entry
this.egw().json(
'EGroupware\\Api\\Etemplate\\Widget\\Placeholder::ajax_fill_placeholders',
[entry.get_value().app, placeholder_list.get_value(), entry.get_value().id],
function(_content)
{
preview_content.set_value(_content);
preview_content.getDOMNode().parentNode.style.visibility = _content.trim() ? null : 'hidden';
}.bind(this)
).sendRequest(true);
}
else
{
preview_content.getDOMNode().parentNode.style.visibility = 'hidden';
}
}
_get_group_options(appname)
{
let options = [];
Object.keys(et2_placeholder_select.placeholders[appname]).map((key) =>
{
options.push(
{
value: key,
label: this.egw().lang(key)
});
});
return options;
}
_get_placeholders(appname, group)
{
let options = [];
Object.keys(et2_placeholder_select.placeholders[appname][group]).map((key) =>
{
options.push(
{
value: key,
label: et2_placeholder_select.placeholders[appname][group][key]
});
});
return options;
}
set_value(value)
{
this.value = value;
}
getValue()
{
return this.value;
}
};
et2_register_widget(et2_placeholder_select, ["placeholder-select"]);

View File

@ -62,6 +62,7 @@ import './et2_widget_image';
import './et2_widget_iframe'; import './et2_widget_iframe';
import './et2_widget_file'; import './et2_widget_file';
import './et2_widget_link'; import './et2_widget_link';
import './et2_widget_placeholder';
import './et2_widget_progress'; import './et2_widget_progress';
import './et2_widget_portlet'; import './et2_widget_portlet';
import './et2_widget_selectAccount'; import './et2_widget_selectAccount';

View File

@ -246,14 +246,21 @@ class Merge extends Api\Storage\Merge
'owner' => lang('Owner'), 'owner' => lang('Owner'),
) as $name => $label) ) as $name => $label)
{ {
if (in_array($name,array('start','end')) && $n&1) // main values, which should be in the first column if(in_array($name, array('start',
'end')) && $n & 1) // main values, which should be in the first column
{ {
echo "</tr>\n"; echo "</tr>\n";
$n++; $n++;
} }
if (!($n&1)) echo '<tr>'; if(!($n & 1))
echo '<td>{{calendar/#/'.$name.'}}</td><td>'.$label.'</td>'; {
if ($n&1) echo "</tr>\n"; echo '<tr>';
}
echo '<td>{{calendar/#/' . $name . '}}</td><td>' . $label . '</td>';
if($n & 1)
{
echo "</tr>\n";
}
$n++; $n++;
} }
echo "</table>\n"; echo "</table>\n";
@ -261,6 +268,55 @@ class Merge extends Api\Storage\Merge
$GLOBALS['egw']->framework->render(ob_get_clean()); $GLOBALS['egw']->framework->render(ob_get_clean());
} }
/**
* Get a list of placeholders provided.
*
* Placeholders are grouped logically. Group key should have a user-friendly translation.
*/
public function get_placeholder_list()
{
$placeholders = [];
$group = 'contact';
foreach($this->contacts->contact_fields as $name => $label)
{
if(in_array($name, array('tid', 'label', 'geo')))
{
continue;
} // dont show them, as they are not used in the UI atm.
switch($name)
{
case 'adr_one_street':
$group = 'business';
break;
case 'adr_two_street':
$group = 'private';
break;
case 'tel_work':
$group = 'phone';
break;
case 'email':
case 'email_home':
$group = 'email';
break;
case 'url':
$group = 'details';
}
$placeholders[$group]["{{" . $name . "}}"] = $label;
if($name == 'cat_id')
{
$placeholders[$group]["{{" . $name . "}}"] = lang('Category path');
}
}
$group = 'customfields';
foreach($this->contacts->customfields as $name => $field)
{
$placeholders[$group]["{{" . $name . "}}"] = $field['label'];
}
return $placeholders;
}
/** /**
* Get insert-in-document action with optional default document on top * Get insert-in-document action with optional default document on top
* *

View File

@ -0,0 +1,207 @@
<?php
/**
* EGroupware - eTemplate serverside of linking widgets
*
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
* @package api
* @subpackage etemplate
* @link http://www.egroupware.org
* @author Nathan Gray
* @copyright 2011 Nathan Gray
* @version $Id$
*/
namespace EGroupware\Api\Etemplate\Widget;
use EGroupware\Api\Etemplate;
use EGroupware\Api;
/**
* eTemplate Placeholder
* Deals with listing & inserting placeholders, usually into Collabora
*/
class Placeholder extends Etemplate\Widget
{
public $public_functions = array(
'ajax_get_placeholders' => true,
'ajax_fill_placeholder' => true
);
/**
* Constructor
*
* @param string|\XMLReader $xml string with xml or XMLReader positioned on the element to construct
* @throws Api\Exception\WrongParameter
*/
public function __construct($xml = '')
{
if($xml)
{
parent::__construct($xml);
}
}
/**
* Set up what we know on the server side.
*
* Set the options for the application select.
*
* @param string $cname
* @param array $expand values for keys 'c', 'row', 'c_', 'row_', 'cont'
*/
public function beforeSendToClient($cname, array $expand = null)
{
}
/**
* Get the placeholders that match the given parameters.
* Default options will get all placeholders in a single request.
*/
public static function ajax_get_placeholders($apps = null, $group = null)
{
$placeholders = [];
if(is_null($apps))
{
$apps = ['addressbook'];
}
foreach($apps as $appname)
{
$merge = Api\Storage\Merge::get_app_class($appname);
switch($appname)
{
case 'user':
$list = $merge->get_user_replacement_list();
break;
default:
$list = $merge->get_placeholder_list();
break;
}
if(!is_null($group))
{
$list = array_intersect_key($list, $group);
}
$placeholders[$appname] = $list;
}
$response = Api\Json\Response::get();
$response->data($placeholders);
}
public function ajax_fill_placeholders($app, $content, $entry)
{
$merge = Api\Storage\Merge::get_app_class($app);
$err = "";
switch($app)
{
case 'addressbook':
default:
$merged = $merge->merge_string($content, [$entry['id']], $err, 'text/plain');
}
$response = Api\Json\Response::get();
$response->data($merged);
}
/**
* Validate input
*
* Following attributes get checked:
* - needed: value must NOT be empty
* - min, max: int and float widget only
* - maxlength: maximum length of string (longer strings get truncated to allowed size)
* - preg: perl regular expression incl. delimiters (set by default for int, float and colorpicker)
* - int and float get casted to their type
*
* @param string $cname current namespace
* @param array $expand values for keys 'c', 'row', 'c_', 'row_', 'cont'
* @param array $content
* @param array &$validated =array() validated content
*/
public function validate($cname, array $expand, array $content, &$validated = array())
{
$form_name = self::form_name($cname, $this->id, $expand);
if(!$this->is_readonly($cname, $form_name))
{
$value = $value_in =& self::get_array($content, $form_name);
// keep values added into request by other ajax-functions, eg. files draged into htmlarea (Vfs)
if((!$value || is_array($value) && !$value['to_id']) && is_array($expand['cont'][$this->id]) && !empty($expand['cont'][$this->id]['to_id']))
{
if(!is_array($value))
{
$value = array(
'to_app' => $expand['cont'][$this->id]['to_app'],
);
}
$value['to_id'] = $expand['cont'][$this->id]['to_id'];
}
// Link widgets can share IDs, make sure to preserve values from others
$already = self::get_array($validated, $form_name);
if($already != null)
{
$value = array_merge($value, $already);
}
// Automatically do link if user selected entry but didn't click 'Link' button
$link = self::get_array($content, self::form_name($cname, $this->id . '_link_entry'));
if($this->type == 'link-to' && is_array($link) && $link['app'] && $link['id'])
{
// Do we have enough information to link automatically?
if(is_array($value) && $value['to_id'])
{
Api\Link::link($value['to_app'], $value['to_id'], $link['app'], $link['id']);
}
else
{
// Not enough information, leave it to the application
if(!is_array($value['to_id']))
{
$value['to_id'] = array();
}
$value['to_id'][] = $link;
}
}
// Look for files - normally handled by ajax
$files = self::get_array($content, self::form_name($cname, $this->id . '_file'));
if(is_array($files) && !(is_array($value) && $value['to_id']))
{
$value = array();
if(is_dir($GLOBALS['egw_info']['server']['temp_dir']) && is_writable($GLOBALS['egw_info']['server']['temp_dir']))
{
$path = $GLOBALS['egw_info']['server']['temp_dir'] . '/';
}
else
{
$path = '';
}
foreach($files as $name => $attrs)
{
if(!is_array($value['to_id']))
{
$value['to_id'] = array();
}
$value['to_id'][] = array(
'app' => Api\Link::VFS_APPNAME,
'id' => array(
'name' => $attrs['name'],
'type' => $attrs['type'],
'tmp_name' => $path . $name
)
);
}
}
$valid =& self::get_array($validated, $form_name, true);
if(true)
{
$valid = $value;
}
//error_log($this);
//error_log(" " . array2string($valid));
}
}
}

View File

@ -1572,6 +1572,25 @@ abstract class Merge
return $app; return $app;
} }
/**
* Get the correct class for the given app
*
* @param $appname
*/
public static function get_app_class($appname)
{
if(class_exists($appname) && is_subclass_of($appname, 'EGroupware\\Api\\Storage\\Merge'))
{
$classname = "{$appname}_merge";
$document_merge = new $classname();
}
else
{
$document_merge = new Api\Contacts\Merge();
}
return $document_merge;
}
/** /**
* Get the replacements for any entry specified by app & id * Get the replacements for any entry specified by app & id
* *
@ -1580,7 +1599,7 @@ abstract class Merge
* @param string $content * @param string $content
* @return array * @return array
*/ */
public function get_app_replacements($app, $id, $content, $prefix='') public function get_app_replacements($app, $id, $content, $prefix = '')
{ {
$replacements = array(); $replacements = array();
if($app == 'addressbook') if($app == 'addressbook')

View File

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE overlay PUBLIC "-//EGroupware GmbH//eTemplate 2//EN" "http://www.egroupware.org/etemplate2.dtd">
<!-- $Id$ -->
<overlay>
<template id="etemplate.insert_merge_placeholder" template="" lang="" group="0" version="21.1.001">
<vbox id="outer_box">
<hbox id="selects">
<vbox>
<select id="app"/>
<select id="group"/>
</vbox>
<select id="placeholder_list"/>
</hbox>
<link-entry id="entry" label="Preview with entry"/>
<hbox class="preview">
<description id="preview_placeholder"/>
<button id="insert_placeholder"></button>
</hbox>
<hbox class="preview">
<description id="preview_content"/>
<button id="insert_content"></button>
</hbox>
</vbox>
<styles>
#api\.insert_merge_placeholder_outer_box > #api\.insert_merge_placeholder_selects {
flex: 1 1 80%;
}
#api\.insert_merge_placeholder_outer_box > label.et2_label {
flex: 0 1 auto;
}
#api\.insert_merge_placeholder_outer_box .preview {
flex: 1 1 2em;
font-size: larger;
}
select#api\.insert_merge_placeholder_app {
flex-grow: 0;
}
.ui-dialog-content, div.et2_box_widget, div.et2_box_widget > div.et2_box_widget {
display: flex;
flex: 1 1 auto;
}
div.et2_hbox {
flex-direction: row;
flex-grow: 1;
}
div.et2_vbox {
flex-direction: column;
gap: 5px;
}
div.et2_box_widget > * {
flex: 1 1 auto;
width: 100%;
}
div.et2_link_entry {
flex-grow: 0;
}
div.et2_link_entry input.ui-autocomplete-input {
width: 75%
}
.et2_hbox button {
flex: 0 1 24px;
height: 24px;
}
</styles>
</template>
</overlay>