* Collabora: Merge placeholder dialogs

Added merge placeholder & address dialogs to Collabora.  Also some new merge preferences for target filename and location, and placeholder list UI
This commit is contained in:
nathan 2021-10-14 13:18:21 -06:00
parent 09f93f2b9d
commit 82103dd514
28 changed files with 3259 additions and 1426 deletions

View File

@ -291,31 +291,8 @@ class addressbook_hooks
if ($GLOBALS['egw_info']['user']['apps']['filemanager'])
{
$settings['default_document'] = array(
'type' => 'vfs_file',
'size' => 60,
'label' => 'Default document to insert contacts',
'name' => 'default_document',
'help' => lang('If you specify a document (full vfs path) here, %1 displays an extra document icon for each entry. That icon allows to download the specified document with the data inserted.', lang('addressbook')).' '.
lang('The document can contain placeholder like {{%1}}, to be replaced with the data.','n_fn').' '.
lang('The following document-types are supported:'). implode(',',Api\Storage\Merge::get_file_extensions()),
'run_lang' => false,
'xmlrpc' => True,
'admin' => False,
);
$settings['document_dir'] = array(
'type' => 'vfs_dirs',
'size' => 60,
'label' => 'Directory with documents to insert contacts',
'name' => 'document_dir',
'help' => lang('If you specify a directory (full vfs path) here, %1 displays an action for each document. That action allows to download the specified document with the data inserted.',lang('addressbook')).' '.
lang('The document can contain placeholder like {{%1}}, to be replaced with the data.','n_fn').' '.
lang('The following document-types are supported:'). implode(',',Api\Storage\Merge::get_file_extensions()),
'run_lang' => false,
'xmlrpc' => True,
'admin' => False,
'default' => '/templates/addressbook',
);
$merge = new Api\Contacts\Merge();
$settings += $merge->merge_preferences();
}
if ($GLOBALS['egw_info']['user']['apps']['felamimail'] || $GLOBALS['egw_info']['user']['apps']['mail'])

View File

@ -0,0 +1,562 @@
/**
* 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_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";
import type {et2_button} from "./et2_widget_button";
/**
* 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"
},
dialog_title: {
"name": "Dialog title",
"type": "string",
"default": "Insert Placeholder"
}
};
static placeholders : Object | null = null;
button : JQuery;
submit_callback : any;
dialog : et2_dialog;
protected value : any;
protected LIST_URL = 'EGroupware\\Api\\Etemplate\\Widget\\Placeholder::ajax_get_placeholders';
protected TEMPLATE = '/api/templates/default/insert_merge_placeholder.xet?1';
/**
* 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(
this.LIST_URL,
[],
function(_content)
{
if(typeof _content === 'object' && _content.message)
{
// Something went wrong
this.egw().message(_content.message, 'error');
return;
}
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 placeholder selection dialog
*
* @param {object} _data content
*/
protected _buildDialog(_data)
{
let self = this;
let buttons = [
{
text: this.egw().lang("Insert"),
id: "submit",
image: "export"
}
];
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: '', entry: {}},
sel_options: {app: [], group: []},
modifications: {
outer_box: {
entry: {
application_list: []
}
}
}
};
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;
data.content.entry = {app: data.content.app};
data.modifications.outer_box.entry.application_list = Object.keys(_data);
// Remove non-app placeholders (user & general)
let non_apps = ['user', 'general'];
for(let i = 0; i < non_apps.length; i++)
{
let index = data.modifications.outer_box.entry.application_list.indexOf(non_apps[i]);
data.modifications.outer_box.entry.application_list.splice(index, 1);
}
// callback for dialog
this.submit_callback = function(submit_button_id, submit_value)
{
if((submit_button_id == 'submit' || (extra_buttons_action && extra_buttons_action[submit_button_id])) && submit_value)
{
this._do_insert_callback(submit_value);
return true;
}
}.bind(this);
this.dialog = <et2_dialog>et2_createWidget("dialog",
{
callback: this.submit_callback,
title: this.egw().lang(this.options.dialog_title) || this.egw().lang("Insert Placeholder"),
buttons: buttons,
minWidth: 500,
minHeight: 400,
width: 400,
value: data,
template: this.egw().webserverUrl + this.TEMPLATE,
resizable: true
}, et2_dialog._create_parent('api'));
this.dialog.template.uniqueId = 'api.insert_merge_placeholder';
this.dialog.div.on('load', this._on_template_load.bind(this));
}
doLoadingFinished()
{
this._content.call(this, null);
return true;
}
/**
* Post-load of the dialog
* Bind internal events, set some things that are difficult to do in the template
*/
_on_template_load()
{
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
app.onchange = (node, widget) =>
{
preview.set_value("");
if(['user', 'filemanager'].indexOf(widget.get_value()) >= 0)
{
// These ones don't let you select an entry for preview (they don't work)
entry.set_disabled(true);
entry.app_select.val('user');
entry.set_value({app: 'user', id: '', query: ''});
}
else if(widget.get_value() == 'general')
{
// Don't change entry app, leave it
entry.set_disabled(false);
}
else
{
entry.set_disabled(false);
entry.app_select.val(widget.get_value());
entry.set_value({app: widget.get_value(), id: '', query: ''});
}
let groups = this._get_group_options(widget.get_value());
group.set_select_options(groups);
group.set_value(groups[0].value);
group.onchange();
}
group.onchange = (select_node, select_widget) =>
{
let options = this._get_placeholders(app.get_value(), group.get_value())
placeholder_list.set_select_options(options);
preview.set_value("");
placeholder_list.set_value(options[0].value);
}
placeholder_list.onchange = this._on_placeholder_select.bind(this);
entry.onchange = this._on_placeholder_select.bind(this);
(<et2_button>this.dialog.template.widgetContainer.getDOMWidgetById("insert_placeholder")).onclick = () =>
{
this.options.insert_callback(this.dialog.template.widgetContainer.getDOMWidgetById("preview_placeholder").getDOMNode().textContent);
};
(<et2_button>this.dialog.template.widgetContainer.getDOMWidgetById("insert_content")).onclick = () =>
{
this.options.insert_callback(this.dialog.template.widgetContainer.getDOMWidgetById("preview_content").getDOMNode().textContent);
};
app.set_value(app.get_value());
}
/**
* User has selected a placeholder
* Update the UI, and if they have an entry selected do the replacement and show that.
*/
_on_placeholder_select()
{
let app = <et2_link_entry>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");
// Show the selected placeholder
this.set_value(placeholder_list.get_value());
preview.set_value(placeholder_list.get_value());
preview.getDOMNode().parentNode.style.visibility = placeholder_list.get_value().trim() ? null : 'hidden';
if(placeholder_list.get_value() && entry.get_value())
{
// Show the selected placeholder replaced with value from the selected entry
this.egw().json(
'EGroupware\\Api\\Etemplate\\Widget\\Placeholder::ajax_fill_placeholders',
[placeholder_list.get_value(), entry.get_value()],
function(_content)
{
if(!_content)
{
_content = '';
}
preview_content.set_value(_content);
preview_content.getDOMNode().parentNode.style.visibility = _content.trim() ? null : 'hidden';
}.bind(this)
).sendRequest(true);
}
else
{
// No value, hide the row
preview_content.getDOMNode().parentNode.style.visibility = 'hidden';
}
}
/**
* Get the list of placeholder groups under the selected application
* @param appname
* @returns {value:string, label:string}[]
*/
_get_group_options(appname : string)
{
let options = [];
Object.keys(et2_placeholder_select.placeholders[appname]).map((key) =>
{
// @ts-ignore
if(Object.keys(et2_placeholder_select.placeholders[appname][key]).filter((key) => isNaN(key)).length > 0)
{
// Handle groups of groups
if(typeof et2_placeholder_select.placeholders[appname][key].label !== "undefined")
{
options[key] = et2_placeholder_select.placeholders[appname][key];
}
else
{
options[this.egw().lang(key)] = [];
for(let sub of Object.keys(et2_placeholder_select.placeholders[appname][key]))
{
if(!et2_placeholder_select.placeholders[appname][key][sub])
{
continue;
}
options[this.egw().lang(key)].push({
value: key + '-' + sub,
label: this.egw().lang(sub)
});
}
}
}
else
{
options.push({
value: key,
label: this.egw().lang(key)
});
}
});
return options;
}
/**
* Get a list of placeholders under the given application + group
*
* @param appname
* @param group
* @returns {value:string, label:string}[]
*/
_get_placeholders(appname : string, group : string)
{
let _group = group.split('-', 2);
let ph = et2_placeholder_select.placeholders[appname];
for(let i = 0; typeof ph !== "undefined" && i < _group.length; i++)
{
ph = ph[_group[i]];
}
return ph || [];
}
/**
* Get the correct insert text call the insert callback with it
*
* @param dialog_values
*/
_do_insert_callback(dialog_values : Object)
{
this.options.insert_callback(this.get_value());
}
set_value(value)
{
this.value = value;
}
getValue()
{
return this.value;
}
};
et2_register_widget(et2_placeholder_select, ["placeholder-select"]);
/**
* Display a dialog to choose from a set list of placeholder snippets
*/
export class et2_placeholder_snippet_select extends et2_placeholder_select
{
static readonly _attributes : any = {
dialog_title: {
"default": "Insert address"
}
};
static placeholders = {
"addressbook": {
"addresses": {
"{{org_name}}\n{{n_fn}}\n{{adr_one_street}}{{NELF adr_one_street2}}\n{{adr_one_formatted}}": "Business address",
"{{n_fn}}\n{{adr_two_street}}{{NELF adr_two_street2}}\n{{adr_two_formatted}}": "Home address",
"{{n_fn}}\n{{email}}\n{{tel_work}}": "Name, email, phone"
}
}
};
button : JQuery;
submit_callback : any;
dialog : et2_dialog;
protected value : any;
protected LIST_URL = 'EGroupware\\Api\\Etemplate\\Widget\\Placeholder::ajax_get_placeholders';
protected TEMPLATE = '/api/templates/default/placeholder_snippet.xet?1';
/**
* Post-load of the dialog
* Bind internal events, set some things that are difficult to do in the template
*/
_on_template_load()
{
let app = <et2_selectbox>this.dialog.template.widgetContainer.getDOMWidgetById("app");
let placeholder_list = <et2_selectbox>this.dialog.template.widgetContainer.getDOMWidgetById("placeholder_list");
let preview = <et2_description>this.dialog.template.widgetContainer.getDOMWidgetById("preview_content");
let entry = <et2_link_entry>this.dialog.template.widgetContainer.getDOMWidgetById("entry");
placeholder_list.set_select_options(this._get_placeholders("addressbook", "addresses"));
// Further setup / styling that can't be done in etemplate
app.getInputNode().setAttribute("readonly", true);
this.dialog.template.DOMContainer.style.display = "flex";
this.dialog.template.DOMContainer.firstChild.style.display = "flex";
placeholder_list.getDOMNode().size = 5;
// Bind some handlers
app.onchange = (node, widget) =>
{
entry.set_value({app: widget.get_value()});
placeholder_list.set_select_options(this._get_placeholders(app.get_value(), "addresses"));
}
placeholder_list.onchange = this._on_placeholder_select.bind(this);
entry.onchange = this._on_placeholder_select.bind(this);
app.set_value(app.get_value());
this._on_placeholder_select();
}
/**
* User has selected a placeholder
* Update the UI, and if they have an entry selected do the replacement and show that.
*/
_on_placeholder_select()
{
let app = <et2_link_entry>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");
if(placeholder_list.get_value() && entry.get_value())
{
// Show the selected placeholder replaced with value from the selected entry
this.egw().json(
'EGroupware\\Api\\Etemplate\\Widget\\Placeholder::ajax_fill_placeholders',
[placeholder_list.get_value(), {app: "addressbook", id: entry.get_value()}],
function(_content)
{
if(!_content)
{
_content = '';
}
this.set_value(_content);
preview_content.set_value(_content);
preview_content.getDOMNode().parentNode.style.visibility = _content.trim() ? null : 'hidden';
}.bind(this)
).sendRequest(true);
}
else
{
// No value, hide the row
preview_content.getDOMNode().parentNode.style.visibility = 'hidden';
}
if(!entry.get_value())
{
entry.search.get(0).focus();
}
}
/**
* Get the list of placeholder groups under the selected application
* @param appname
* @returns {value:string, label:string}[]
*/
_get_group_options(appname : string)
{
let options = [];
Object.keys(et2_placeholder_select.placeholders[appname]).map((key) =>
{
options.push(
{
value: key,
label: this.egw().lang(key)
});
});
return options;
}
/**
* Get a list of placeholders under the given application + group
*
* @param appname
* @param group
* @returns {value:string, label:string}[]
*/
_get_placeholders(appname : string, group : string)
{
let options = [];
Object.keys(et2_placeholder_snippet_select.placeholders[appname][group]).map((key) =>
{
options.push(
{
value: key,
label: this.egw().lang(et2_placeholder_snippet_select.placeholders[appname][group][key])
});
});
return options;
}
/**
* Get the correct insert text call the insert callback with it
*
* @param dialog_values
*/
_do_insert_callback(dialog_values : Object)
{
this.options.insert_callback(this.get_value());
}
set_value(value)
{
this.value = value;
}
getValue()
{
return this.value;
}
};
et2_register_widget(et2_placeholder_snippet_select, ["placeholder-snippet"]);

View File

@ -738,7 +738,7 @@ var et2_selectbox = /** @class */ (function (_super) {
if (sub == 'value')
continue;
if (typeof _options[key][sub] === 'object' && _options[key][sub] !== null) {
this._appendOptionElement(sub, _options[key][sub]["label"] ? _options[key][sub]["label"] : "", _options[key][sub]["title"] ? _options[key][sub]["title"] : "", group);
this._appendOptionElement(typeof _options[key][sub]["value"] !== "undefined" ? _options[key][sub]["value"] : sub, _options[key][sub]["label"] ? _options[key][sub]["label"] : "", _options[key][sub]["title"] ? _options[key][sub]["title"] : "", group);
}
else {
this._appendOptionElement(sub, _options[key][sub], undefined, group);

View File

@ -982,7 +982,8 @@ export class et2_selectbox extends et2_inputWidget
if(sub == 'value') continue;
if (typeof _options[key][sub] === 'object' && _options[key][sub] !== null)
{
this._appendOptionElement(sub,
this._appendOptionElement(
typeof _options[key][sub]["value"] !== "undefined" ? _options[key][sub]["value"] : sub,
_options[key][sub]["label"] ? _options[key][sub]["label"] : "",
_options[key][sub]["title"] ? _options[key][sub]["title"] : "",
group

View File

@ -50,6 +50,7 @@
et2_widget_file;
et2_widget_link;
et2_widget_progress;
et2_widget_placeholder;
et2_widget_portlet;
et2_widget_selectAccount;
et2_widget_ajaxSelect;

View File

@ -49,6 +49,7 @@
et2_widget_file;
et2_widget_link;
et2_widget_progress;
et2_widget_placeholder;
et2_widget_portlet;
et2_widget_selectAccount;
et2_widget_ajaxSelect;

View File

@ -274,6 +274,7 @@ choose a background style. common de Wählen Sie einen Hintergrundstil.
choose a text color for the icons common de Wählen Sie eine Textfarbe für die Symbole
choose file... common de Dateien wählen...
choose the category common de Kategorie auswählen
choose the default filename for merged documents. preferences de Wählen Sie den Standard-Dateinamen für zusammengeführte Platzhalter-Dokumente.
choose the parent category common de Wählen der übergeordneten Kategorie
choose time common de Uhrzeit auswählen
chosen parent category no longer exists common de Die ausgewählte Elternkategorie existiert nicht (mehr).
@ -378,6 +379,7 @@ december common de Dezember
deck common de Deck (intern)
default common de Vorgabe
default category common de Standard-Kategorie
default document to insert entries preferences de Standarddokument für Einfügen in Dokument
default height for the windows common de Vorgabewert für Höhe des Fensters
default visible actions common de standardmäßig sichtbare Aktionen
default width for the windows common de Vorgabewert für Breite des Fensters
@ -417,6 +419,8 @@ diable the execution a bugfixscript for internet explorer 5.5 and higher to show
direction left to right common de Richtung von links nach rechts
directory common de Verzeichnis
directory does not exist, is not readable by the webserver or is not relative to the document root! common de Verzeichnis existiert nicht, ist nicht vom Webserver lesbar oder ist nicht entsprechend zur Dokumentroot!
directory for storing merged documents preferences de Verzeichnis für zusammengeführte Platzhalter-Dokumente
directory with documents to insert entries preferences de Vorlagen-Verzeichnis für Einfügen in Dokument
disable internet explorer png-image-bugfix common de Internet Explorer PNG-Bilder-Bugfix abschalten
disable slider effects common de Schwebeeffekte des Navigationsmenüs abschalten
disable the animated slider effects when showing or hiding menus in the page? opera and konqueror users will probably must want this. common de Die animierten Schwebeeffekte beim Anzeigen oder Verstecken des Navigationsmenüs in der Seite abschalten? Benutzer von Opera oder Konquerer müssen diese Funktion abschalten.
@ -724,6 +728,7 @@ insert new column behind this one common de Neue Spalte hinter dieser einfügen
insert new column in front of all common de Neue Spalte vor dieser einfügen
insert new row after this one common de Neue Zeile nach dieser einfügen
insert new row in front of first line common de Neue Zeile vor dieser einfügen
insert placeholder common de Platzhalter einfügen
insert row after common de Zeile danach einfügen
insert row before common de Zeile davor einfügen
insert timestamp into description field common de Zeitstempel in das Beschreibungs-Feld einfügen
@ -1072,6 +1077,7 @@ preference common de Einstellung
preferences common de Einstellungen
preferences for the %1 template set preferences de Einstellungen für das %1 Template
prev common de Vorheriger
preview with entry common de Vorschau aus Eintrag
previous common de Vorherige
previous page common de Vorherige Seite
primary group common de Hauptgruppe
@ -1498,6 +1504,7 @@ western sahara common de WEST SAHARA
what color should all the blank space on the desktop have common de Welche Farbe soll der freie Platz auf der Arbeitsfläche haben
what happens with overflowing content: visible (default), hidden, scroll, auto (browser decides) common de was passiert mit überbreitem Inhalt: sichtbar (standard), versteckt, rollend, automatisch (der Browser entscheidet)
what style would you like the image to have? common de Welchen Stil soll das Bild haben?
when you merge entries into documents, they will be stored here. If no directory is provided, they will be stored in your home directory (%1) preferences de Wenn Sie Einträge mit Platzhalter-Dokumenten zusammenführen, werden diese hier gespeichert. Wenn Sie kein Verzeichnis angeben, werden diese in Ihrem Homeverzeichnis gespeichert (%1)
when you say yes the home and logout buttons are presented as applications in the main top applcation bar. common de Wenn Sie dies aktivieren, werden die Start und Abmelde Symbole als Anwendungen im oberen Anwendungsbalken angezeigt.
where and how will the egroupware links like preferences, about and logout be displayed. common de Wo und wie werden die EGroupware Verknüpfungen wie Einstellungen, Über ..., und Abmelden angezeigt.
which groups common de Welche Gruppen

View File

@ -274,6 +274,7 @@ choose a background style. common en Choose a background style
choose a text color for the icons common en Choose a text color for the icons
choose file... common en Choose file...
choose the category common en Choose the category
choose the default filename for merged documents. preferences en Choose the default filename for merged documents.
choose the parent category common en Choose the parent category
choose time common en Choose Time
chosen parent category no longer exists common en Chosen parent category no longer exists
@ -378,6 +379,7 @@ december common en December
deck common en Deck (internal)
default common en Default
default category common en Default category
default document to insert entries preferences en Default document to insert entries
default height for the windows common en Default height for the windows
default visible actions common en Default visible actions
default width for the windows common en Default width for the windows
@ -417,6 +419,8 @@ diable the execution a bugfixscript for internet explorer 5.5 and higher to show
direction left to right common en Direction left to right
directory common en Directory
directory does not exist, is not readable by the webserver or is not relative to the document root! common en Directory does not exist, is not readable by the web server or is not relative to the document root!
directory for storing merged documents preferences en Directory for storing merged documents
directory with documents to insert entries preferences en Directory with documents to insert entries
disable internet explorer png-image-bugfix common en Disable Internet Explorer png image bugfix
disable slider effects common en Disable slider effects
disable the animated slider effects when showing or hiding menus in the page? opera and konqueror users will probably must want this. common en Disable the animated slider effects when showing or hiding menus in the page.
@ -724,6 +728,7 @@ insert new column behind this one common en Insert new column after
insert new column in front of all common en Insert new column before all
insert new row after this one common en Insert new row after
insert new row in front of first line common en Insert new row before first line
insert placeholder common en Insert placeholder
insert row after common en Insert row after
insert row before common en Insert row before
insert timestamp into description field common en Insert timestamp into description field
@ -855,6 +860,7 @@ maybe common en Maybe
mayotte common en MAYOTTE
medium common en Medium
menu common en Menu
merged document filename preferences en Merged document filename
message common en Message
message ... common en Message ...
message prepared for sending. common en Message prepared for sending.
@ -1072,6 +1078,7 @@ preference common en Preference
preferences common en Preferences
preferences for the %1 template set preferences en Preferences for the %1 template set
prev common en Prev
preview with entry common en Preview with entry
previous common en Previous
previous page common en Previous page
primary group common en Primary group
@ -1498,6 +1505,7 @@ western sahara common en WESTERN SAHARA
what color should all the blank space on the desktop have common en What color should all the blank space on the desktop have?
what happens with overflowing content: visible (default), hidden, scroll, auto (browser decides) common en What happens with overflowing content: visible (default), hidden, scroll, auto (browser decides)
what style would you like the image to have? common en Image style
when you merge entries into documents, they will be stored here. If no directory is provided, they will be stored in your home directory (%1) preferences en When you merge entries into documents, they will be stored here. If no directory is provided, they will be stored in your home directory (%1)
when you say yes the home and logout buttons are presented as applications in the main top applcation bar. common en If you say yes, the Home and Log out buttons are presented as applications in the main top application bar.
where and how will the egroupware links like preferences, about and logout be displayed. common en Where and how will the EGroupware links like Preferences, About and Log out be displayed.
which groups common en Which groups

View File

@ -156,109 +156,139 @@ class Merge extends Api\Storage\Merge
}
/**
* Generate table with replacements for the preferences
* Get a list of placeholders provided.
*
* Placeholders are grouped logically. Group key should have a user-friendly translation.
*/
public function show_replacements()
public function get_placeholder_list($prefix = '')
{
$GLOBALS['egw_info']['flags']['app_header'] = lang('Addressbook').' - '.lang('Replacements for inserting contacts into documents');
$GLOBALS['egw_info']['flags']['nonavbar'] = (bool)$_GET['nonavbar'];
// Specific order for these ones
$placeholders = [
'contact' => [],
'details' => [
[
'value' => $this->prefix($prefix, 'categories', '{'),
'label' => lang('Category path')
],
['value' => $this->prefix($prefix, 'note', '{'),
'label' => $this->contacts->contact_fields['note']],
['value' => $this->prefix($prefix, 'id', '{'),
'label' => $this->contacts->contact_fields['id']],
['value' => $this->prefix($prefix, 'owner', '{'),
'label' => $this->contacts->contact_fields['owner']],
['value' => $this->prefix($prefix, 'private', '{'),
'label' => $this->contacts->contact_fields['private']],
['value' => $this->prefix($prefix, 'cat_id', '{'),
'label' => $this->contacts->contact_fields['cat_id']],
],
ob_start();
echo "<table width='90%' align='center'>\n";
echo '<tr><td colspan="4"><h3>'.lang('Contact fields:')."</h3></td></tr>";
];
$n = 0;
// Iterate through the list & switch groups as we go
// Hopefully a little better than assigning each field to a group
$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.
if (in_array($name,array('email','org_name','tel_work','url')) && $n&1) // main values, which should be in the first column
if(in_array($name, array('tid', 'label', 'geo')))
{
echo "</tr>\n";
$n++;
}
if (!($n&1)) echo '<tr>';
echo '<td>{{'.$name.'}}</td><td>'.$label.'</td>';
if($name == 'cat_id')
continue;
} // dont show them, as they are not used in the UI atm.
switch($name)
{
if ($n&1) echo "</tr>\n";
echo '<td>{{categories}}</td><td>'.lang('Category path').'</td>';
$n++;
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 'freebusy_uri':
$group = 'details';
}
$marker = $this->prefix($prefix, $name, '{');
if(!array_filter($placeholders, function ($a) use ($marker)
{
count(array_filter($a, function ($b) use ($marker)
{
return $b['value'] == $marker;
})
) > 0;
}))
{
$placeholders[$group][] = [
'value' => $marker,
'label' => $label
];
}
if ($n&1) echo "</tr>\n";
$n++;
}
echo '<tr><td colspan="4"><h3>'.lang('Custom fields').":</h3></td></tr>";
foreach($this->contacts->customfields as $name => $field)
// Correctly formatted address by country / preference
$placeholders['business'][] = [
'value' => $this->prefix($prefix, 'adr_one_formatted', '{'),
'label' => "Formatted business address"
];
$placeholders['private'][] = [
'value' => $this->prefix($prefix, 'adr_two_formatted', '{'),
'label' => "Formatted private address"
];
$placeholders['EPL only'][] = [
'value' => $this->prefix($prefix, 'share', '{'),
'label' => 'Public sharing URL'
];
$this->add_customfield_placeholders($placeholders, $prefix);
// Don't add any linked placeholders if we're not at the top level
// This avoids potential recursion
if(!$prefix)
{
echo '<tr><td>{{#'.$name.'}}</td><td colspan="3">'.$field['label']."</td></tr>\n";
$this->add_calendar_placeholders($placeholders, $prefix);
}
echo '<tr><td colspan="4"><h3>'.lang('General fields:')."</h3></td></tr>";
foreach(array(
'link' => lang('HTML link to the current record'),
'links' => lang('Titles of any entries linked to the current record, excluding attached files'),
'attachments' => lang('List of files linked to the current record'),
'links_attachments' => lang('Links and attached files'),
'links/[appname]' => lang('Links to specified application. Example: {{links/infolog}}'),
'date' => lang('Date'),
'user/n_fn' => lang('Name of current user, all other contact fields are valid too'),
'user/account_lid' => lang('Username'),
'pagerepeat' => lang('For serial letter use this tag. Put the content, you want to repeat between two Tags.'),
'label' => lang('Use this tag for addresslabels. Put the content, you want to repeat, between two tags.'),
'labelplacement' => lang('Tag to mark positions for address labels'),
'IF fieldname' => lang('Example {{IF n_prefix~Mr~Hello Mr.~Hello Ms.}} - search the field "n_prefix", for "Mr", if found, write Hello Mr., else write Hello Ms.'),
'NELF' => lang('Example {{NELF role}} - if field role is not empty, you will get a new line with the value of field role'),
'NENVLF' => lang('Example {{NENVLF role}} - if field role is not empty, set a LF without any value of the field'),
'LETTERPREFIX' => lang('Example {{LETTERPREFIX}} - Gives a letter prefix without double spaces, if the title is empty for example'),
'LETTERPREFIXCUSTOM' => lang('Example {{LETTERPREFIXCUSTOM n_prefix title n_family}} - Example: Mr Dr. James Miller'),
) as $name => $label)
{
echo '<tr><td>{{'.$name.'}}</td><td colspan="3">'.$label."</td></tr>\n";
}
echo '<tr><td colspan="4"><h3>'.lang('EPL Only').":</h3></td></tr>";
echo '<tr><td>{{share}}</td><td colspan="3">'.lang('Public sharing URL')."</td></tr>\n";
return $placeholders;
}
protected function add_calendar_placeholders(&$placeholders, $prefix)
{
Api\Translation::add_app('calendar');
echo '<tr><td colspan="4"><h3>'.lang('Calendar fields:')." # = 1, 2, ..., 20, -1</h3></td></tr>";
foreach(array(
'title' => lang('Title'),
'description' => lang('Description'),
'participants' => lang('Participants'),
'location' => lang('Location'),
'start' => lang('Start').': '.lang('Date').'+'.lang('Time'),
'startday' => lang('Start').': '.lang('Weekday'),
'startdate'=> lang('Start').': '.lang('Date'),
'starttime'=> lang('Start').': '.lang('Time'),
'end' => lang('End').': '.lang('Date').'+'.lang('Time'),
'endday' => lang('End').': '.lang('Weekday'),
'enddate' => lang('End').': '.lang('Date'),
'endtime' => lang('End').': '.lang('Time'),
'duration' => lang('Duration'),
'category' => lang('Category'),
'priority' => lang('Priority'),
'updated' => lang('Updated'),
'recur_type' => lang('Repetition'),
'access' => lang('Access').': '.lang('public').', '.lang('private'),
'owner' => lang('Owner'),
) as $name => $label)
{
if (in_array($name,array('start','end')) && $n&1) // main values, which should be in the first column
{
echo "</tr>\n";
$n++;
}
if (!($n&1)) echo '<tr>';
echo '<td>{{calendar/#/'.$name.'}}</td><td>'.$label.'</td>';
if ($n&1) echo "</tr>\n";
$n++;
}
echo "</table>\n";
$GLOBALS['egw']->framework->render(ob_get_clean());
// NB: The -1 is actually 1, a non-breaking hyphen to avoid UI issues where we split on -
$group = lang('Calendar fields:') . " # = 1, 2, ..., 20, 1";
foreach(array(
'title' => lang('Title'),
'description' => lang('Description'),
'participants' => lang('Participants'),
'location' => lang('Location'),
'start' => lang('Start') . ': ' . lang('Date') . '+' . lang('Time'),
'startday' => lang('Start') . ': ' . lang('Weekday'),
'startdate' => lang('Start') . ': ' . lang('Date'),
'starttime' => lang('Start') . ': ' . lang('Time'),
'end' => lang('End') . ': ' . lang('Date') . '+' . lang('Time'),
'endday' => lang('End') . ': ' . lang('Weekday'),
'enddate' => lang('End') . ': ' . lang('Date'),
'endtime' => lang('End') . ': ' . lang('Time'),
'duration' => lang('Duration'),
'category' => lang('Category'),
'priority' => lang('Priority'),
'updated' => lang('Updated'),
'recur_type' => lang('Repetition'),
'access' => lang('Access') . ': ' . lang('public') . ', ' . lang('private'),
'owner' => lang('Owner'),
) as $name => $label)
{
$placeholders[$group][] = array(
'value' => $this->prefix(($prefix ? $prefix . '/' : '') . 'calendar/#', $name, '{'),
'label' => $label
);
}
}
/**

View File

@ -0,0 +1,237 @@
<?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 = array_merge(
['addressbook', 'user', 'general'],
// We use linking for preview, so limit to apps that support links
array_keys(Api\Link::app_list('query')),
// Filemanager doesn't support links, but add it anyway
['filemanager']
);
}
foreach($apps as $appname)
{
$merge = Api\Storage\Merge::get_app_class($appname);
switch($appname)
{
case 'user':
$list = $merge->get_user_placeholder_list();
break;
case 'general':
$list = $merge->get_common_placeholder_list();
break;
default:
if(get_class($merge) === 'EGroupware\Api\Contacts\Merge' && $appname !== 'addressbook' || $placeholders[$appname])
{
// Looks like app doesn't support merging
continue 2;
}
Api\Translation::load_app($appname, $GLOBALS['egw_info']['user']['preferences']['common']['lang']);
$list = method_exists($merge, 'get_placeholder_list') ? $merge->get_placeholder_list() : [];
break;
}
if(!is_null($group) && is_array($list))
{
$list = array_intersect_key($list, $group);
}
// Remove if empty
foreach($list as $p_group => $p_list)
{
if(count($p_list) == 0)
{
unset($list[$p_group]);
}
}
if($list)
{
$placeholders[$appname] = $list;
}
}
$response = Api\Json\Response::get();
$response->data($placeholders);
}
public function ajax_fill_placeholders($content, $entry)
{
$merge = Api\Storage\Merge::get_app_class($entry['app']);
$err = "";
switch($entry['app'])
{
case 'user':
$entry = ['id' => $GLOBALS['egw_info']['user']['person_id']];
// fall through
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));
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,86 @@
<?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>
<hbox class="preview">
<description id="preview_placeholder"/>
<button id="insert_placeholder" label="Insert" statustext="Insert placeholder" image="export"></button>
</hbox>
<hrule/>
<link-entry id="entry" label="Preview with entry"/>
<hbox class="preview">
<description id="preview_content"/>
<button id="insert_content" label="Insert" statustext="Insert merged content" image="export"></button>
</hbox>
</vbox>
<styles>
/** Structural stuff **/
#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%
}
div.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset button, button#cancel, .et2_button {
border: none;
border-radius: 0px;
background-color: transparent;
}
div.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset button:hover, button#cancel:hover {
box-shadow: none;
-webkit-box-shadow: none;
}
.preview .et2_button {
flex: 0 1 24px;
height: 24px;
border: none;
border-radius: 0px;
background-color: transparent;
}
/** Cosmetics **/
#api\.insert_merge_placeholder_outer_box option:first-letter {
text-transform: capitalize;
}
</styles>
</template>
</overlay>

View File

@ -0,0 +1,72 @@
<?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.placeholder_snippet" template="" lang="" group="0" version="21.1.001">
<vbox id="outer_box">
<vbox id="selects">
<select id="app"/>
<select id="placeholder_list"/>
</vbox>
<hrule/>
<link-entry id="entry" label="Select entry" only_app="addressbook"/>
<hbox class="preview">
<description id="preview_content"/>
</hbox>
</vbox>
<styles>
#api\.insert_merge_placeholder_outer_box > #api\.insert_merge_placeholder_selects {
flex: 1 1 50%;
}
#api\.insert_merge_placeholder_outer_box > label.et2_label {
flex: 0 1 auto;
}
#api\.insert_merge_placeholder_outer_box .preview {
flex: 1 1 50%;
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: 70%
}
div.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset button, button#cancel, .et2_button {
border: none;
border-radius: 0px;
background-color: transparent;
}
div.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset button:hover, button#cancel:hover {
box-shadow: none;
-webkit-box-shadow: none;
}
.preview .et2_button {
flex: 0 1 24px;
height: 24px;
border: none;
border-radius: 0px;
background-color: transparent;
}
</styles>
</template>
</overlay>

View File

@ -0,0 +1,65 @@
<?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="api.show_replacements.placeholder_list">
<description id="title" class="title"/>
<grid id="placeholders" width="100%">
<columns>
<column width="30%"/>
<column/>
</columns>
<rows>
<row>
<description id="${row}[value]"/>
<description id="${row}[label]"/>
</row>
</rows>
</grid>
</template>
<template id="api.show_replacements" template="" lang="" group="0" version="21.1.001">
<vbox>
<description value="Placeholders" class="group title"/>
<box id="placeholders">
<box id="${row}">
<template template="api.show_replacements.placeholder_list"/>
</box>
</box>
<template template="@extra_template"/>
<details title="Common">
<description value="Common" class="group title"/>
<box id="common">
<box id="${row}">
<template template="api.show_replacements.placeholder_list"/>
</box>
</box>
</details>
<details title="Current user">
<description value="Current user" class="group title"/>
<box id="user">
<box id="${row}">
<template template="api.show_replacements.placeholder_list"/>
</box>
</box>
</details>
</vbox>
<styles>
.et2_details_title, .title {
display: inline-block;
font-weight: bold;
font-size: 130%;
margin-top: 2ex;
}
.et2_details_title, .group {
margin-top: 3ex;
font-size: 150%;
}
/** Cosmetics **/
#api-show_replacements_title:first-letter, .title {
text-transform: capitalize;
}
</styles>
</template>
</overlay>

View File

@ -22,11 +22,13 @@ require_once realpath(__DIR__.'/../WidgetBaseTest.php');
use EGroupware\Api\Etemplate;
class EntryTest extends \EGroupware\Api\Etemplate\WidgetBaseTest {
class ContactEntryTest extends \EGroupware\Api\Etemplate\WidgetBaseTest
{
const TEST_TEMPLATE = 'api.entry_test_contact';
public static function setUpBeforeClass() : void {
public static function setUpBeforeClass() : void
{
parent::setUpBeforeClass();
}

View File

@ -12,6 +12,7 @@
namespace EGroupware\Api\Storage;
require_once __DIR__ . '/../LoggedInTest.php';
use EGroupware\Api\LoggedInTest as LoggedInTest;
class CustomfieldsTest extends LoggedInTest
@ -20,19 +21,19 @@ class CustomfieldsTest extends LoggedInTest
protected $customfields = null;
protected $simple_field = array(
'app' => self::APP,
'name' => 'test_field',
'label' => 'Custom field',
'type' => 'text',
'type2' => array(),
'help' => 'Custom field created for automated testing by CustomfieldsTest',
'values' => null,
'len' => null,
'rows' => null,
'order' => null,
'needed' => null,
'private' => array()
);
'app' => self::APP,
'name' => 'test_field',
'label' => 'Custom field',
'type' => 'text',
'type2' => array(),
'help' => 'Custom field created for automated testing by CustomfieldsTest',
'values' => null,
'len' => null,
'rows' => null,
'order' => null,
'needed' => null,
'private' => array()
);
public function tearDown(): void
{
@ -209,35 +210,35 @@ class CustomfieldsTest extends LoggedInTest
// Expected options, file
return array(
array(array(
'' => 'Select',
'Α'=> 'α Alpha',
'Β'=> 'β Beta',
'Γ'=> 'γ Gamma',
'Δ'=> 'δ Delta',
'Ε'=> 'ε Epsilon',
'Ζ'=> 'ζ Zeta',
'Η'=> 'η Eta',
'Θ'=> 'θ Theta',
'Ι'=> 'ι Iota',
'Κ'=> 'κ Kappa',
'Λ'=> 'λ Lambda',
'Μ'=> 'μ Mu',
'Ν'=> 'ν Nu',
'Ξ'=> 'ξ Xi',
'Ο'=> 'ο Omicron',
'Π'=> 'π Pi',
'Ρ'=> 'ρ Rho',
'Σ'=> 'σ Sigma',
'Τ'=> 'τ Tau',
'Υ'=> 'υ Upsilon',
'Φ'=> 'φ Phi',
'Χ'=> 'χ Chi',
'Ψ'=> 'ψ Psi',
'Ω'=> 'ω Omega'
), 'greek_options.php'),
'' => 'Select',
'Α' => 'α Alpha',
'Β' => 'β Beta',
'Γ' => 'γ Gamma',
'Δ' => 'δ Delta',
'Ε' => 'ε Epsilon',
'Ζ' => 'ζ Zeta',
'Η' => 'η Eta',
'Θ' => 'θ Theta',
'Ι' => 'ι Iota',
'Κ' => 'κ Kappa',
'Λ' => 'λ Lambda',
'Μ' => 'μ Mu',
'Ν' => 'ν Nu',
'Ξ' => 'ξ Xi',
'Ο' => 'ο Omicron',
'Π' => 'π Pi',
'Ρ' => 'ρ Rho',
'Σ' => 'σ Sigma',
'Τ' => 'τ Tau',
'Υ' => 'υ Upsilon',
'Φ' => 'φ Phi',
'Χ' => 'χ Chi',
'Ψ' => 'ψ Psi',
'Ω' => 'ω Omega'
), 'greek_options.php'),
array(array(
'View Subs' => "egw_open('','infolog','list',{action:'sp',action_id:widget.getRoot().getArrayMgr('content').getEntry('info_id')},'infolog','infolog');"
), 'infolog_subs_option.php')
'View Subs' => "egw_open('','infolog','list',{action:'sp',action_id:widget.getRoot().getArrayMgr('content').getEntry('info_id')},'infolog','infolog');"
), 'infolog_subs_option.php')
);
}
@ -302,8 +303,8 @@ class CustomfieldsTest extends LoggedInTest
protected function get_another_user()
{
$accounts = $GLOBALS['egw']->accounts->search(array(
'type' => 'accounts'
));
'type' => 'accounts'
));
unset($accounts[$GLOBALS['egw_info']['user']['account_id']]);
if(count($accounts) == 0)
{

View File

@ -14,8 +14,10 @@
namespace EGroupware\Api\Storage;
require_once __DIR__ . '/../../src/Storage/Tracking.php';
class TestTracking extends Tracking {
class TestTracking extends Tracking
{
var $app = 'test';

View File

@ -12,6 +12,7 @@
namespace EGroupware\Api\Storage;
require_once __DIR__ . '/../LoggedInTest.php';
require_once __DIR__ . '/TestTracking.php';
use EGroupware\Api;
@ -22,18 +23,18 @@ class TrackingTest extends LoggedInTest
const APP = 'test';
protected $simple_field = array(
'app' => self::APP,
'name' => 'test_field',
'label' => 'Custom field',
'type' => 'text',
'type2' => array(),
'help' => 'Custom field created for automated testing by CustomfieldsTest',
'values' => null,
'len' => null,
'rows' => null,
'order' => null,
'needed' => null,
'private' => array()
'app' => self::APP,
'name' => 'test_field',
'label' => 'Custom field',
'type' => 'text',
'type2' => array(),
'help' => 'Custom field created for automated testing by CustomfieldsTest',
'values' => null,
'len' => null,
'rows' => null,
'order' => null,
'needed' => null,
'private' => array()
);
/**
@ -57,8 +58,8 @@ class TrackingTest extends LoggedInTest
// Get another user
$accounts = $GLOBALS['egw']->accounts->search(array(
'type' => 'accounts'
));
'type' => 'accounts'
));
unset($accounts[$GLOBALS['egw_info']['user']['account_id']]);
if(count($accounts) == 0)
{

View File

@ -296,15 +296,18 @@ class SharingBase extends LoggedInTest
*/
protected function mountVersioned($path)
{
if (!class_exists('EGroupware\Stylite\Vfs\Versioning\StreamWrapper'))
if(!class_exists('EGroupware\Stylite\Vfs\Versioning\StreamWrapper'))
{
$this->markTestSkipped("No versioning available");
}
if(substr($path, -1) == '/') $path = substr($path, 0, -1);
if(substr($path, -1) == '/')
{
$path = substr($path, 0, -1);
}
$backup = Vfs::$is_root;
Vfs::$is_root = true;
$url = Versioning\StreamWrapper::PREFIX.$path;
$this->assertTrue(Vfs::mount($url,$path), "Unable to mount $path as versioned");
$url = Versioning\StreamWrapper::PREFIX . $path;
$this->assertTrue(Vfs::mount($url, $path, false), "Unable to mount $path as versioned");
Vfs::$is_root = $backup;
$this->mounts[] = $path;
@ -362,8 +365,8 @@ class SharingBase extends LoggedInTest
Vfs::chmod($path, 0750);
Vfs::chown($path, $GLOBALS['egw_info']['user']['account_id']);
$url = \EGroupware\Stylite\Vfs\Merge\StreamWrapper::SCHEME.'://default'.$path.'?merge=' . realpath(__DIR__ . '/../fixtures/Vfs/filesystem_mount');
$this->assertTrue(Vfs::mount($url,$path), "Unable to mount $url to $path");
$url = \EGroupware\Stylite\Vfs\Merge\StreamWrapper::SCHEME . '://default' . $path . '?merge=' . realpath(__DIR__ . '/../fixtures/Vfs/filesystem_mount');
$this->assertTrue(Vfs::mount($url, $path, false), "Unable to mount $url to $path");
Vfs::$is_root = $backup;
$this->mounts[] = $path;

View File

@ -660,31 +660,8 @@ class calendar_hooks
// Merge print
if ($GLOBALS['egw_info']['user']['apps']['filemanager'])
{
$settings['default_document'] = array(
'type' => 'vfs_file',
'size' => 60,
'label' => 'Default document to insert entries',
'name' => 'default_document',
'help' => lang('If you specify a document (full vfs path) here, %1 displays an extra document icon for each entry. That icon allows to download the specified document with the data inserted.',lang('calendar')).' '.
lang('The document can contain placeholder like {{%1}}, to be replaced with the data.','calendar_title').' '.
lang('The following document-types are supported:'). implode(',',Api\Storage\Merge::get_file_extensions()),
'run_lang' => false,
'xmlrpc' => True,
'admin' => False,
);
$settings['document_dir'] = array(
'type' => 'vfs_dirs',
'size' => 60,
'label' => 'Directory with documents to insert entries',
'name' => 'document_dir',
'help' => lang('If you specify a directory (full vfs path) here, %1 displays an action for each document. That action allows to download the specified document with the data inserted.',lang('calendar')).' '.
lang('The document can contain placeholder like {{%1}}, to be replaced with the data.','calendar_title').' '.
lang('The following document-types are supported:'). implode(',',Api\Storage\Merge::get_file_extensions()),
'run_lang' => false,
'xmlrpc' => True,
'admin' => False,
'default' => '/templates/calendar',
);
$merge = new calendar_merge();
$settings += $merge->merge_preferences();
}
$settings += array(

File diff suppressed because it is too large Load Diff

View File

@ -161,52 +161,29 @@ class filemanager_hooks
'forced' => 'yes',
),
'showusers' => array(
'type' => 'select',
'name' => 'showusers',
'values' => $yes_no,
'label' => lang('Show link "%1" in side box menu?',lang('Users and groups')),
'xmlrpc' => True,
'admin' => False,
'forced' => 'yes',
'type' => 'select',
'name' => 'showusers',
'values' => $yes_no,
'label' => lang('Show link "%1" in side box menu?', lang('Users and groups')),
'xmlrpc' => True,
'admin' => False,
'forced' => 'yes',
),
);
$settings['default_document'] = array(
'type' => 'vfs_file',
'size' => 60,
'label' => 'Default document to insert entries',
'name' => 'default_document',
'help' => lang('If you specify a document (full vfs path) here, %1 displays an extra document icon for each entry. That icon allows to download the specified document with the data inserted.',lang('filemanager')).' '.
lang('The document can contain placeholder like {{%1}}, to be replaced with the data.', 'name').' '.
lang('The following document-types are supported:'). implode(',',Api\Storage\Merge::get_file_extensions()),
'run_lang' => false,
'xmlrpc' => True,
'admin' => False,
);
$settings['document_dir'] = array(
'type' => 'vfs_dirs',
'size' => 60,
'label' => 'Directory with documents to insert entries',
'name' => 'document_dir',
'help' => lang('If you specify a directory (full vfs path) here, %1 displays an action for each document. That action allows to download the specified document with the %1 data inserted.', lang('filemanager')).' '.
lang('The document can contain placeholder like {{%1}}, to be replaced with the data.','name').' '.
lang('The following document-types are supported:'). implode(',',Api\Storage\Merge::get_file_extensions()),
'run_lang' => false,
'xmlrpc' => True,
'admin' => False,
'default' => '/templates/filemanager',
);
$merge = new filemanager_merge();
$settings += $merge->merge_preferences();
$editorLink = self::getEditorLink();
$mimes = array('0' => lang('None'));
foreach ((array)$editorLink['mime'] as $mime => $value)
foreach((array)$editorLink['mime'] as $mime => $value)
{
$mimes[$mime] = lang('%1 file', strtoupper($value['ext'])).' ('.$mime.')';
$mimes[$mime] = lang('%1 file', strtoupper($value['ext'])) . ' (' . $mime . ')';
if (!empty($value['extra_extensions']))
if(!empty($value['extra_extensions']))
{
$mimes[$mime] .= ', '.strtoupper(implode(', ', $value['extra_extensions']));
$mimes[$mime] .= ', ' . strtoupper(implode(', ', $value['extra_extensions']));
}
}

View File

@ -26,15 +26,14 @@ class filemanager_merge extends Api\Storage\Merge
* @var array
*/
var $public_functions = array(
'show_replacements' => true,
'merge_entries' => true
'show_replacements' => true,
'merge_entries' => true
);
/**
* Fields that are numeric, for special numeric handling
*/
protected $numeric_fields = array(
);
protected $numeric_fields = array();
/**
* Fields that are dates or timestamps
@ -74,12 +73,12 @@ class filemanager_merge extends Api\Storage\Merge
* Get replacements
*
* @param int $id id of entry
* @param string &$content=null content to create some replacements only if they are use
* @param string &$content =null content to create some replacements only if they are use
* @return array|boolean
*/
protected function get_replacements($id,&$content=null)
protected function get_replacements($id, &$content = null)
{
if (!($replacements = $this->filemanager_replacements($id, '', $content)))
if(!($replacements = $this->filemanager_replacements($id, '', $content)))
{
return false;
}
@ -90,58 +89,58 @@ class filemanager_merge extends Api\Storage\Merge
* Get filemanager replacements
*
* @param int $id id (vfs path) of entry
* @param string $prefix='' prefix like eg. 'erole'
* @param string $prefix ='' prefix like eg. 'erole'
* @return array|boolean
*/
public function filemanager_replacements($id,$prefix='', &$content = null)
public function filemanager_replacements($id, $prefix = '', &$content = null)
{
$info = array();
$file = Vfs::lstat($id,true);
$file = Vfs::lstat($id, true);
$file['mtime'] = Api\DateTime::to($file['mtime']);
$file['ctime'] = Api\DateTime::to($file['ctime']);
$file['name'] = Vfs::basename($id);
$file['dir'] = ($dir = Vfs::dirname($id)) ? Vfs::decodePath($dir) : '';
$dirlist = explode('/',$file['dir']);
$dirlist = explode('/', $file['dir']);
$file['folder'] = array_pop($dirlist);
$file['folder_file'] = $file['folder'] . '/'.$file['name'];
$file['folder_file'] = $file['folder'] . '/' . $file['name'];
$file['path'] = $id;
$file['rel_path'] = str_replace($this->dir.'/', '', $id);
$file['rel_path'] = str_replace($this->dir . '/', '', $id);
$file['hsize'] = Vfs::hsize($file['size']);
$file['mime'] = Vfs::mime_content_type($id);
$file['gid'] *= -1; // our widgets use negative gid's
if (($props = Vfs::propfind($id)))
if(($props = Vfs::propfind($id)))
{
foreach($props as $prop)
{
$file[$prop['name']] = $prop['val'];
}
}
if (($file['is_link'] = Vfs::is_link($id)))
if(($file['is_link'] = Vfs::is_link($id)))
{
$file['symlink'] = Vfs::readlink($id);
}
// Custom fields
if($content && strpos($content, '#') !== 0)
{
{
// Expand link-to custom fields
$this->cf_link_to_expand($file, $content, $info);
$this->cf_link_to_expand($file, $content, $info);
foreach(Api\Storage\Customfields::get('filemanager') as $name => $field)
{
// Set any missing custom fields, or the marker will stay
if(!$file['#'.$name])
if(!$file['#' . $name])
{
$file['#'.$name] = '';
$file['#' . $name] = '';
continue;
}
// Format date cfs per user Api\Preferences
if($field['type'] == 'date' || $field['type'] == 'date-time')
{
$this->date_fields[] = '#'.$name;
$file['#'.$name] = Api\DateTime::to($file['#'.$name], $field['type'] == 'date' ? true : '');
$this->date_fields[] = '#' . $name;
$file['#' . $name] = Api\DateTime::to($file['#' . $name], $field['type'] == 'date' ? true : '');
}
}
}
@ -150,17 +149,19 @@ class filemanager_merge extends Api\Storage\Merge
if($dirlist[1] == 'apps' && count($dirlist) > 1)
{
// Try this first - a normal path /apps/appname/id/file
list($app, $app_id) = explode('/', substr($file['path'], strpos($file['path'], 'apps/')+5));
list($app, $app_id) = explode('/', substr($file['path'], strpos($file['path'], 'apps/') + 5));
// Symlink?
if(!$app || !(int)$app_id || !array_key_exists($app, $GLOBALS['egw_info']['user']['apps'])) {
if(!$app || !(int)$app_id || !array_key_exists($app, $GLOBALS['egw_info']['user']['apps']))
{
// Try resolving just app + ID - /apps/App Name/Record Title/file
$resolved = Vfs::resolve_url_symlinks(implode('/',array_slice(explode('/',$file['dir']),0,4)));
list($app, $app_id) = explode('/', substr($resolved, strpos($resolved, 'apps/')+5));
$resolved = Vfs::resolve_url_symlinks(implode('/', array_slice(explode('/', $file['dir']), 0, 4)));
list($app, $app_id) = explode('/', substr($resolved, strpos($resolved, 'apps/') + 5));
if(!$app || !(int)$app_id || !array_key_exists($app, $GLOBALS['egw_info']['user']['apps'])) {
if(!$app || !(int)$app_id || !array_key_exists($app, $GLOBALS['egw_info']['user']['apps']))
{
// Get rid of any virtual folders (eg: All$) and symlinks
$resolved = Vfs::resolve_url_symlinks($file['path']);
list($app, $app_id) = explode('/', substr($resolved, strpos($resolved, 'apps/')+5));
list($app, $app_id) = explode('/', substr($resolved, strpos($resolved, 'apps/') + 5));
}
}
if($app && $app_id)
@ -170,7 +171,7 @@ class filemanager_merge extends Api\Storage\Merge
$app_merge = null;
try
{
$classname = $app .'_merge';
$classname = $app . '_merge';
if(class_exists($classname))
{
$app_merge = new $classname();
@ -180,9 +181,10 @@ class filemanager_merge extends Api\Storage\Merge
}
}
}
// Silently discard & continue
catch(Exception $e) {
unset($e); // not used
// Silently discard & continue
catch (Exception $e)
{
unset($e); // not used
}
}
}
@ -211,7 +213,7 @@ class filemanager_merge extends Api\Storage\Merge
foreach($file as $key => &$value)
{
if(!$value) $value = '';
$info['$$'.($prefix ? $prefix.'/':'').$key.'$$'] = $value;
$info['$$' . ($prefix ? $prefix . '/' : '') . $key . '$$'] = $value;
}
if($app_placeholders)
{
@ -239,14 +241,18 @@ class filemanager_merge extends Api\Storage\Merge
{
return $session;
}
else if (($session = \EGroupware\Api\Cache::getSession(Api\Sharing::class, "$app::$id")) &&
substr($session['share_path'], -strlen($path)) === $path)
else
{
return $session;
if(($session = \EGroupware\Api\Cache::getSession(Api\Sharing::class, "$app::$id")) &&
substr($session['share_path'], -strlen($path)) === $path)
{
return $session;
}
}
// Need to create the share here.
// No way to know here if it should be writable, or who it's going to
$mode = /* ? ? Sharing::WRITABLE :*/ Api\Sharing::READONLY;
$mode = /* ? ? Sharing::WRITABLE :*/
Api\Sharing::READONLY;
$recipients = array();
$extra = array();
@ -254,72 +260,59 @@ class filemanager_merge extends Api\Storage\Merge
}
/**
* Generate table with replacements for the Api\Preferences
* Hook for extending apps to customise the replacements UI without having to override the whole method
*
* @param string $template_name
* @param $content
* @param $sel_options
* @param $readonlys
*/
public function show_replacements()
protected function show_replacements_hook(&$template_name, &$content, &$sel_options, &$readonlys)
{
$GLOBALS['egw_info']['flags']['app_header'] = lang('filemanager').' - '.lang('Replacements for inserting entries into documents');
$GLOBALS['egw_info']['flags']['nonavbar'] = false;
echo $GLOBALS['egw']->framework->header();
$content['extra_template'] = 'filemanager.replacements';
}
echo "<table width='90%' align='center'>\n";
echo '<tr><td colspan="4"><h3>'.lang('Filemanager fields:')."</h3></td></tr>";
/**
* Get a list of placeholders provided.
*
* Placeholders are grouped logically. Group key should have a user-friendly translation.
*/
public function get_placeholder_list($prefix = '')
{
$placeholders = parent::get_placeholder_list($prefix);
$n = 0;
$fields = array(
'name' => 'name',
'path' => 'Absolute path',
'rel_path' => 'Path relative to current directory',
'folder' => 'Containing folder',
'name' => 'name',
'path' => 'Absolute path',
'rel_path' => 'Path relative to current directory',
'folder' => 'Containing folder',
'folder_file' => 'Containing folder and file name',
'url' => 'url',
'webdav_url' => 'External path using webdav',
'link' => 'Clickable link to file',
'comment' => 'comment',
'mtime' => 'modified',
'ctime' => 'created',
'mime' => 'Type',
'hsize' => 'Size',
'size' => 'Size (in bytes)',
'url' => 'url',
'webdav_url' => 'External path using webdav',
'link' => 'Clickable link to file',
'comment' => 'comment',
'mtime' => 'modified',
'ctime' => 'created',
'mime' => 'Type',
'hsize' => 'Size',
'size' => 'Size (in bytes)',
);
$group = 'placeholders';
foreach($fields as $name => $label)
{
if (!($n&1)) echo '<tr>';
echo '<td>{{'.$name.'}}</td><td>'.lang($label).'</td>';
if ($n&1) echo "</tr>\n";
$n++;
$marker = $this->prefix($prefix, $name, '{');
if(!array_filter($placeholders, function ($a) use ($marker)
{
return array_key_exists($marker, $a);
}))
{
$placeholders[$group][] = [
'value' => $marker,
'label' => $label
];
}
}
echo '<tr><td colspan="4"><h3>'.lang('Custom fields').":</h3></td></tr>";
foreach(Api\Storage\Customfields::get('filemanager') as $name => $field)
{
echo '<tr><td>{{#'.$name.'}}</td><td colspan="3">'.$field['label']."</td></tr>\n";
}
echo '<tr><td colspan="4"><h3>'.lang('Application fields').":</h3></td></tr>";
echo '<tr><td colspan="4">'.lang('For files linked to an application entry (inside /apps/appname/id/) the placeholders for that application are also available. See the specific application for a list of available placeholders.').'</td></tr>';
echo '<tr><td colspan="4"><h3>'.lang('General fields:')."</h3></td></tr>";
foreach(array(
'date' => lang('Date'),
'user/n_fn' => lang('Name of current user, all other contact fields are valid too'),
'user/account_lid' => lang('Username'),
'pagerepeat' => lang('For serial letter use this tag. Put the content, you want to repeat between two Tags.'),
'label' => lang('Use this tag for addresslabels. Put the content, you want to repeat, between two tags.'),
'labelplacement' => lang('Tag to mark positions for address labels'),
'IF fieldname' => lang('Example {{IF n_prefix~Mr~Hello Mr.~Hello Ms.}} - search the field "n_prefix", for "Mr", if found, write Hello Mr., else write Hello Ms.'),
'NELF' => lang('Example {{NELF role}} - if field role is not empty, you will get a new line with the value of field role'),
'NENVLF' => lang('Example {{NENVLF role}} - if field role is not empty, set a LF without any value of the field'),
'LETTERPREFIX' => lang('Example {{LETTERPREFIX}} - Gives a letter prefix without double spaces, if the title is empty for example'),
'LETTERPREFIXCUSTOM' => lang('Example {{LETTERPREFIXCUSTOM n_prefix title n_family}} - Example: Mr Dr. James Miller'),
) as $name => $label)
{
echo '<tr><td>{{'.$name.'}}</td><td colspan="3">'.$label."</td></tr>\n";
}
echo "</table>\n";
echo $GLOBALS['egw']->framework->footer();
return $placeholders;
}
}

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE overlay PUBLIC "-//EGroupware GmbH//eTemplate 2//EN" "http://www.egroupware.org/etemplate2.dtd">
<!-- This template adds the extra bits to the replacements list UI -->
<overlay>
<template id="filemanager.replacements">
<vbox>
<description class="title" value="Application fields"/>
<description
value="For files linked to an application entry (inside /apps/appname/id/) the placeholders for that application are also available. See the specific application for a list of available placeholders."/>
</vbox>
</template>
</overlay>

View File

@ -458,31 +458,8 @@ class infolog_hooks
// Merge print
if ($GLOBALS['egw_info']['user']['apps']['filemanager'])
{
$settings['default_document'] = array(
'type' => 'vfs_file',
'size' => 60,
'label' => 'Default document to insert entries',
'name' => 'default_document',
'help' => lang('If you specify a document (full vfs path) here, %1 displays an extra document icon for each entry. That icon allows to download the specified document with the data inserted.',lang('infolog')).' '.
lang('The document can contain placeholder like {{%1}}, to be replaced with the data.','info_subject').' '.
lang('The following document-types are supported:').'*.rtf, *.txt',
'run_lang' => false,
'xmlrpc' => True,
'admin' => False,
);
$settings['document_dir'] = array(
'type' => 'vfs_dirs',
'size' => 60,
'label' => 'Directory with documents to insert entries',
'name' => 'document_dir',
'help' => lang('If you specify a directory (full vfs path) here, %1 displays an action for each document. That action allows to download the specified document with the data inserted.',lang('infolog')).' '.
lang('The document can contain placeholder like {{%1}}, to be replaced with the data.','info_subject').' '.
lang('The following document-types are supported:').'*.rtf, *.txt',
'run_lang' => false,
'xmlrpc' => True,
'admin' => False,
'default' => '/templates/infolog',
);
$merge = new infolog_merge();
$settings += $merge->merge_preferences();
}
if ($GLOBALS['egw_info']['user']['apps']['calendar'])

View File

@ -64,23 +64,42 @@ class infolog_merge extends Api\Storage\Merge
* @param string &$content=null content to create some replacements only if they are use
* @return array|boolean
*/
protected function get_replacements($id,&$content=null)
protected function get_replacements($id, &$content = null)
{
if (!($replacements = $this->infolog_replacements($id, '', $content)))
if(!($replacements = $this->infolog_replacements($id, '', $content)))
{
return false;
}
return $replacements;
}
/**
* Override contact filename placeholder to use info_contact
*
* @param $document
* @param $ids
* @return array|void
*/
public function get_filename_placeholders($document, $ids)
{
$placeholders = parent::get_filename_placeholders($document, $ids);
if(count($ids) == 1 && ($info = $this->bo->read($ids[0])))
{
$placeholders['$$contact_title$$'] = $info['info_contact']['title'] ??
(is_array($info['info_contact']) && Link::title($info['info_contact']['app'], $info['info_contact']['id'])) ??
'';
}
return $placeholders;
}
/**
* Get infolog replacements
*
* @param int $id id of entry
* @param string $prefix='' prefix like eg. 'erole'
* @param string $prefix ='' prefix like eg. 'erole'
* @return array|boolean
*/
public function infolog_replacements($id,$prefix='', &$content = '')
public function infolog_replacements($id, $prefix = '', &$content = '')
{
$record = new infolog_egw_record($id);
$info = array();
@ -179,84 +198,61 @@ class infolog_merge extends Api\Storage\Merge
return $info;
}
/**
* Generate table with replacements for the Api\Preferences
*
*/
public function show_replacements()
public function get_placeholder_list($prefix = '')
{
$GLOBALS['egw_info']['flags']['app_header'] = lang('infolog').' - '.lang('Replacements for inserting entries into documents');
$GLOBALS['egw_info']['flags']['nonavbar'] = false;
echo $GLOBALS['egw']->framework->header();
echo "<table width='90%' align='center'>\n";
echo '<tr><td colspan="4"><h3>'.lang('Infolog fields:')."</h3></td></tr>";
$n = 0;
$tracking = new infolog_tracking($this->bo);
$fields = array('info_id' => lang('Infolog ID'), 'pm_id' => lang('Project ID'), 'project' => lang('Project name')) + $tracking->field2label + array('info_sum_timesheets' => lang('Used time'));
$placeholders = array(
'infolog' => [],
lang('parent') => [],
lang($tracking->field2label['info_from']) => []
) + parent::get_placeholder_list($prefix);
$fields = array('info_id' => lang('Infolog ID'), 'pm_id' => lang('Project ID'),
'project' => lang('Project name')) + $tracking->field2label + array('info_sum_timesheets' => lang('Used time'));
Api\Translation::add_app('projectmanager');
$group = 'infolog';
foreach($fields as $name => $label)
{
if (in_array($name,array('custom'))) continue; // dont show them
if (in_array($name,array('info_subject', 'info_des')) && $n&1) // main values, which should be in the first column
if(in_array($name, array('custom')))
{
echo "</tr>\n";
$n++;
// dont show them
continue;
}
if (!($n&1)) echo '<tr>';
echo '<td>{{'.$name.'}}</td><td>'.lang($label).'</td>';
if ($n&1) echo "</tr>\n";
$n++;
}
echo '<tr><td colspan="4"><h3>'.lang('Custom fields').":</h3></td></tr>";
$contact_custom = false;
foreach($this->bo->customfields as $name => $field)
{
echo '<tr><td>{{#'.$name.'}}</td><td colspan="3">'.$field['label'].($field['type'] == 'select-account' ? '*':'')."</td></tr>\n";
if($field['type'] == 'select-account') $contact_custom = true;
}
if($contact_custom)
{
echo '<tr><td /><td colspan="3">* '.lang('Addressbook placeholders available'). '</td></tr>';
}
echo '<tr><td colspan="4"><h3>'.lang('Parent').":</h3></td></tr>";
echo '<tr><td>{{info_id_parent/info_subject}}</td><td colspan="3">'.lang('All other %1 fields are valid',lang('infolog'))."</td></tr>\n";
echo '<tr><td colspan="4"><h3>'.lang('Contact fields').':</h3></td></tr>';
$i = 0;
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.
if (in_array($name,array('email','org_name','tel_work','url')) && $n&1) // main values, which should be in the first column
$marker = $this->prefix($prefix, $name, '{');
if(!array_filter($placeholders, function ($a) use ($marker)
{
echo "</tr>\n";
$i++;
return array_key_exists($marker, $a);
}))
{
$placeholders[$group][] = [
'value' => $marker,
'label' => $label
];
}
if (!($i&1)) echo '<tr>';
echo '<td>{{info_contact/'.$name.'}}</td><td>'.$label.'</td>';
if ($i&1) echo "</tr>\n";
$i++;
}
echo '<tr><td colspan="4"><h3>'.lang('Custom fields').":</h3></td></tr>";
foreach($this->contacts->customfields as $name => $field)
// Don't add any linked placeholders if we're not at the top level
// This avoids potential recursion
if(!$prefix)
{
echo '<tr><td>{{info_contact/#'.$name.'}}</td><td colspan="3">'.$field['label']."</td></tr>\n";
}
// Add contact placeholders
$contact_merge = new Api\Contacts\Merge();
$contact = $contact_merge->get_placeholder_list($this->prefix($prefix, 'info_contact'));
$this->add_linked_placeholders($placeholders, lang($tracking->field2label['info_from']), $contact);
echo '<tr><td colspan="4"><h3>'.lang('General fields:')."</h3></td></tr>";
foreach($this->get_common_replacements() as $name => $label)
// Add parent placeholders
$this->add_linked_placeholders(
$placeholders,
lang('parent'),
$this->get_placeholder_list(($prefix ? $prefix . '/' : '') . 'info_id_parent')
);
}
else
{
echo '<tr><td>{{'.$name.'}}</td><td colspan="3">'.$label."</td></tr>\n";
unset($placeholders[lang('parent')]);
unset($placeholders[lang($tracking->field2label['info_from'])]);
}
echo "</table>\n";
echo $GLOBALS['egw']->framework->footer();
return $placeholders;
}
}

View File

@ -178,31 +178,8 @@ class timesheet_hooks
// Merge print
if ($GLOBALS['egw_info']['user']['apps']['filemanager'])
{
$settings['default_document'] = array(
'type' => 'vfs_file',
'size' => 60,
'label' => 'Default document to insert entries',
'name' => 'default_document',
'help' => lang('If you specify a document (full vfs path) here, %1 displays an extra document icon for each entry. That icon allows to download the specified document with the data inserted.',lang('timesheet')).' '.
lang('The document can contain placeholder like {{%1}}, to be replaced with the data.', 'ts_title').' '.
lang('The following document-types are supported:'). implode(',',Api\Storage\Merge::get_file_extensions()),
'run_lang' => false,
'xmlrpc' => True,
'admin' => False,
);
$settings['document_dir'] = array(
'type' => 'vfs_dirs',
'size' => 60,
'label' => 'Directory with documents to insert entries',
'name' => 'document_dir',
'help' => lang('If you specify a directory (full vfs path) here, %1 displays an action for each document. That action allows to download the specified document with the %1 data inserted.', lang('timesheet')).' '.
lang('The document can contain placeholder like {{%1}}, to be replaced with the data.','ts_title').' '.
lang('The following document-types are supported:'). implode(',',Api\Storage\Merge::get_file_extensions()),
'run_lang' => false,
'xmlrpc' => True,
'admin' => False,
'default' => '/templates/timesheet',
);
$merge = new timesheet_merge();
$settings += $merge->merge_preferences();
}
return $settings;

View File

@ -157,64 +157,51 @@ class timesheet_merge extends Api\Storage\Merge
}
/**
* Generate table with replacements for the Api\Preferences
* Get a list of placeholders provided.
*
* Placeholders are grouped logically. Group key should have a user-friendly translation.
*/
public function show_replacements()
public function get_placeholder_list($prefix = '')
{
$GLOBALS['egw_info']['flags']['app_header'] = lang('timesheet').' - '.lang('Replacements for inserting entries into documents');
$GLOBALS['egw_info']['flags']['nonavbar'] = false;
echo $GLOBALS['egw']->framework->header();
$placeholders = array(
'timesheet' => [],
lang('Project') => []
) + parent::get_placeholder_list($prefix);
echo "<table width='90%' align='center'>\n";
echo '<tr><td colspan="4"><h3>'.lang('Timesheet fields:')."</h3></td></tr>";
$n = 0;
$fields = array('ts_id' => lang('Timesheet ID')) + $this->bo->field2label + array(
'ts_total' => lang('total'),
'ts_created' => lang('Created'),
'ts_modified' => lang('Modified'),
);
'ts_total' => lang('total'),
'ts_created' => lang('Created'),
'ts_modified' => lang('Modified'),
);
$group = 'timesheet';
foreach($fields as $name => $label)
{
if (in_array($name,array('pl_id','customfields'))) continue; // dont show them
if (in_array($name,array('ts_title', 'ts_description')) && $n&1) // main values, which should be in the first column
if(in_array($name, array('custom')))
{
echo "</tr>\n";
$n++;
// dont show them
continue;
}
$marker = $this->prefix($prefix, $name, '{');
if(!array_filter($placeholders, function ($a) use ($marker)
{
return array_key_exists($marker, $a);
}))
{
$placeholders[$group][] = [
'value' => $marker,
'label' => $label
];
}
if (!($n&1)) echo '<tr>';
echo '<td>{{'.$name.'}}</td><td>'.lang($label).'</td>';
if ($n&1) echo "</tr>\n";
$n++;
}
echo '<tr><td colspan="4"><h3>'.lang('Custom fields').":</h3></td></tr>";
foreach($this->bo->customfields as $name => $field)
// Don't add any linked placeholders if we're not at the top level
// This avoids potential recursion
if(!$prefix)
{
echo '<tr><td>{{#'.$name.'}}</td><td colspan="3">'.$field['label']."</td></tr>\n";
// Add project placeholders
$pm_merge = new projectmanager_merge();
$this->add_linked_placeholders($placeholders, lang('Project'), $pm_merge->get_placeholder_list('ts_project'));
}
echo '<tr><td colspan="4"><h3>'.lang('Project fields').':</h3></td></tr>';
$pm_merge = new projectmanager_merge();
$i = 0;
foreach($pm_merge->projectmanager_fields as $name => $label)
{
if (!($i&1)) echo '<tr>';
echo '<td>{{ts_project/'.$name.'}}</td><td>'.$label.'</td>';
if ($i&1) echo "</tr>\n";
$i++;
}
echo '<tr><td colspan="4"><h3>'.lang('General fields:')."</h3></td></tr>";
foreach($this->get_common_replacements() as $name => $label)
{
echo '<tr><td>{{'.$name.'}}</td><td colspan="3">'.$label."</td></tr>\n";
}
echo "</table>\n";
echo $GLOBALS['egw']->framework->footer();
return $placeholders;
}
}