diff --git a/api/src/Contacts/Merge.php b/api/src/Contacts/Merge.php
index 4dc6d5a47c..169e233abc 100644
--- a/api/src/Contacts/Merge.php
+++ b/api/src/Contacts/Merge.php
@@ -155,119 +155,6 @@ class Merge extends Api\Storage\Merge
return $replacements;
}
- /**
- * Generate table with replacements for the preferences
- *
- */
- public function show_replacements()
- {
- $GLOBALS['egw_info']['flags']['app_header'] = lang('Addressbook').' - '.lang('Replacements for inserting contacts into documents');
- $GLOBALS['egw_info']['flags']['nonavbar'] = (bool)$_GET['nonavbar'];
-
- ob_start();
- echo "
\n";
- echo ''.lang('Contact fields:')." |
";
-
- $n = 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
- {
- echo "\n";
- $n++;
- }
- if (!($n&1)) echo '';
- echo '{{'.$name.'}} | '.$label.' | ';
- if($name == 'cat_id')
- {
- if ($n&1) echo "
\n";
- echo '{{categories}} | '.lang('Category path').' | ';
- $n++;
- }
- if ($n&1) echo "\n";
- $n++;
- }
-
- echo ''.lang('Custom fields').": |
";
- foreach($this->contacts->customfields as $name => $field)
- {
- echo '{{#'.$name.'}} | '.$field['label']." |
\n";
- }
-
- echo ''.lang('General fields:')." |
";
- 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 '{{'.$name.'}} | '.$label." |
\n";
- }
-
- echo ''.lang('EPL Only').": |
";
- echo '{{share}} | '.lang('Public sharing URL')." |
\n";
-
- Api\Translation::add_app('calendar');
- echo ''.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)
- {
- if(in_array($name, array('start',
- 'end')) && $n & 1) // main values, which should be in the first column
- {
- echo "\n";
- $n++;
- }
- if(!($n & 1))
- {
- echo '';
- }
- echo '{{calendar/#/' . $name . '}} | ' . $label . ' | ';
- if($n & 1)
- {
- echo "
\n";
- }
- $n++;
- }
- echo "
\n";
-
- $GLOBALS['egw']->framework->render(ob_get_clean());
- }
-
/**
* Get a list of placeholders provided.
*
@@ -352,10 +239,58 @@ class Merge extends Api\Storage\Merge
'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)
+ {
+ $this->add_calendar_placeholders($placeholders, $prefix);
+ }
+
return $placeholders;
}
+ protected function add_calendar_placeholders(&$placeholders, $prefix)
+ {
+ Api\Translation::add_app('calendar');
+
+ // 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
+ );
+ }
+ }
+
/**
* Get insert-in-document action with optional default document on top
*
diff --git a/api/src/Storage/Merge.php b/api/src/Storage/Merge.php
index 3dd5924ac4..9e4e5e00a2 100644
--- a/api/src/Storage/Merge.php
+++ b/api/src/Storage/Merge.php
@@ -13,6 +13,7 @@
namespace EGroupware\Api\Storage;
+use ceLTIc\LTI\Service\Result;
use DOMDocument;
use EGroupware\Api;
use EGroupware\Api\Vfs;
@@ -433,11 +434,11 @@ abstract class Merge
*
* Uses egw_link system to get link titles
*
- * @param app Name of current app
- * @param id ID of current entry
- * @param only_app Restrict links to only given application
- * @param exclude Exclude links to these applications
- * @param style String One of:
+ * @param string app Name of current app
+ * @param string id ID of current entry
+ * @param string only_app Restrict links to only given application
+ * @param string[] exclude Exclude links to these applications
+ * @param string style One of:
* 'title' - plain text, just the title of the link
* 'link' - URL to the entry
* 'href' - HREF tag wrapped around the title
@@ -1785,7 +1786,7 @@ abstract class Merge
/**
* Get the replacements for any entry specified by app & id
*
- * @param stribg $app
+ * @param string $app
* @param string $id
* @param string $content
* @return array
@@ -1846,10 +1847,10 @@ abstract class Merge
/**
* Process special flags, such as IF or NELF
*
- * @param content Text to be examined and changed
- * @param replacements array of markers => replacement
+ * @param string content Text to be examined and changed
+ * @param array replacements array of markers => replacement
*
- * @return changed content
+ * @return string changed content
*/
private function process_commands($content, $replacements)
{
@@ -2031,7 +2032,7 @@ abstract class Merge
* @param array $ids array with contact id(s)
* @param string $name ='' name to use for downloaded document
* @param string $dirs comma or whitespace separated directories, used if $document is a relative path
- * @param Array $header File name, mime & filesize if you want to send a header
+ * @param array $header File name, mime & filesize if you want to send a header
*
* @return string with error-message on error
* @throws Api\Exception
@@ -2546,8 +2547,8 @@ abstract class Merge
* back to the client like the other documents. Merging for a single selected
* contact opens a compose window, multiple contacts just sends.
*
- * @param Array &$action Action to be modified for mail
- * @param Array $file Array of information about the document from Api\Vfs::find
+ * @param array &$action Action to be modified for mail
+ * @param array $file Array of information about the document from Api\Vfs::find
* @return void
*/
private static function document_mail_action(array &$action, $file)
@@ -2580,8 +2581,8 @@ abstract class Merge
* Set up a document action so the generated file is saved and opened in
* the collabora editor (if collabora is available)
*
- * @param Array &$action Action to be modified for editor
- * @param Array $file Array of information about the document from Api\Vfs::find
+ * @param array &$action Action to be modified for editor
+ * @param array $file Array of information about the document from Api\Vfs::find
* @return void
*/
private static function document_editable_action(array &$action, $file)
@@ -3198,4 +3199,80 @@ abstract class Merge
return $settings;
}
+
+ /**
+ * Show replacement placeholders for the app
+ *
+ * Generates a page that shows all the available placeholders for this appliction. By default,
+ * we have all placeholders generated in get_placeholder_list() (including any custom fields)
+ * as well as the common and current user placeholders.
+ *
+ * By overridding show_replacements_hook(), extending classes can override without having to
+ * re-implement everything.
+ */
+ public function show_replacements()
+ {
+
+ $template_name = 'api.show_replacements';
+ $content = $sel_options = $readonlys = $preserve = array();
+
+ $content['appname'] = $this->get_app();
+ $content['placeholders'] = $this->remap_replacement_list($this->get_placeholder_list());
+ $content['extra'] = array();
+ $content['common'] = $this->remap_replacement_list($this->get_common_placeholder_list());
+ $content['user'] = $this->remap_replacement_list($this->get_user_placeholder_list());
+
+ $this->show_replacements_hook($template_name, $content, $sel_options, $readonlys);
+ $etemplate = new Api\Etemplate($template_name);
+
+ $etemplate->exec('filemanager.filemanager_ui.file', $content, $sel_options, $readonlys, $preserve, 2);
+ }
+
+ /**
+ * Helper function for show_replacements() to change the output of get_placeholder_list() into somethig
+ * more suited for etemplate repeating rows.
+ *
+ * @param $list
+ * @return array
+ */
+ protected function remap_replacement_list($list, $title_prefix = '')
+ {
+ $new_list = [];
+ foreach($list as $group_title => $group_placeholders)
+ {
+ if(is_array($group_placeholders) && !array_key_exists('0', $group_placeholders))
+ {
+ // Limit how far we go through linked entries
+ if($title_prefix)
+ {
+ continue;
+ }
+ $new_list = array_merge($new_list, $this->remap_replacement_list($group_placeholders, $group_title));
+ }
+ else
+ {
+ $new_list[] = [
+ 'title' => ($title_prefix ? $title_prefix . ': ' : '') . $group_title,
+ 'placeholders' => $group_placeholders
+ ];
+ }
+ }
+ return $new_list;
+ }
+
+ /**
+ * Hook for extending apps to customise the replacements UI without having to override the whole method.
+ *
+ * This can include detailed descriptions or instructions, documentation of tables and custom stuff
+ * Set $content['extra_template'] to a template ID with extra descriptions or instructions and it will be
+ * added into the main template.
+ *
+ * @param string $template_name
+ * @param $content
+ * @param $sel_options
+ * @param $readonlys
+ */
+ protected function show_replacements_hook(&$template_name, &$content, &$sel_options, &$readonlys)
+ {
+ }
}
diff --git a/api/templates/default/show_replacements.xet b/api/templates/default/show_replacements.xet
new file mode 100644
index 0000000000..576563f3d8
--- /dev/null
+++ b/api/templates/default/show_replacements.xet
@@ -0,0 +1,65 @@
+
+
+
+\n";
- echo ''.lang('Infolog fields:')." |
";
-
- $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'));
- Api\Translation::add_app('projectmanager');
- 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
- {
- echo "\n";
- $n++;
- }
- if (!($n&1)) echo '';
- echo '{{'.$name.'}} | '.lang($label).' | ';
- if ($n&1) echo "
\n";
- $n++;
- }
-
- echo ''.lang('Custom fields').": |
";
- $contact_custom = false;
- foreach($this->bo->customfields as $name => $field)
- {
- echo '{{#'.$name.'}} | '.$field['label'].($field['type'] == 'select-account' ? '*':'')." |
\n";
- if($field['type'] == 'select-account') $contact_custom = true;
- }
- if($contact_custom)
- {
- echo ' | * '.lang('Addressbook placeholders available'). ' |
';
- }
-
- echo ''.lang('Parent').": |
";
- echo '{{info_id_parent/info_subject}} | '.lang('All other %1 fields are valid',lang('infolog'))." |
\n";
-
- echo ''.lang('Contact fields').': |
';
- $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
- {
- echo "\n";
- $i++;
- }
- if (!($i&1)) echo '';
- echo '{{info_contact/'.$name.'}} | '.$label.' | ';
- if ($i&1) echo "
\n";
- $i++;
- }
- echo ''.lang('Owner contact fields also available under info_owner/...').' |
';
-
- echo ''.lang('Custom fields').": |
";
- foreach($this->contacts->customfields as $name => $field)
- {
- echo '{{info_contact/#'.$name.'}} | '.$field['label']." |
\n";
- }
-
- echo '' . lang('General fields:') . " |
";
- foreach($this->get_common_replacements() as $name => $label)
- {
- echo '{{' . $name . '}} | ' . $label . " |
\n";
- }
-
- echo "
\n";
-
- echo $GLOBALS['egw']->framework->footer();
- }
-
public function get_placeholder_list($prefix = '')
{
- $placeholders = parent::get_placeholder_list($prefix);
-
$tracking = new infolog_tracking($this->bo);
+ $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 = 'placeholders';
+ $group = 'infolog';
foreach($fields as $name => $label)
{
if(in_array($name, array('custom')))
@@ -310,15 +232,22 @@ class infolog_merge extends Api\Storage\Merge
}
}
- // Add contact placeholders
- $insert_index = 1;
- $placeholders = array_slice($placeholders, 0, $insert_index, true) +
- [lang($tracking->field2label['info_from']) => []] +
- array_slice($placeholders, $insert_index, count($placeholders) - $insert_index, true);
- $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);
+ // Don't add any linked placeholders if we're not at the top level
+ // This avoids potential recursion
+ if(!$prefix)
+ {
+ // 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);
+ // Add parent placeholders
+ $this->add_linked_placeholders(
+ $placeholders,
+ lang('parent'),
+ $this->get_placeholder_list(($prefix ? $prefix . '/' : '') . 'info_id_parent')
+ );
+ }
return $placeholders;
}
}
diff --git a/timesheet/inc/class.timesheet_merge.inc.php b/timesheet/inc/class.timesheet_merge.inc.php
index 89a11149bc..6f4c8c9f1d 100644
--- a/timesheet/inc/class.timesheet_merge.inc.php
+++ b/timesheet/inc/class.timesheet_merge.inc.php
@@ -156,68 +156,6 @@ class timesheet_merge extends Api\Storage\Merge
return $info;
}
- /**
- * Generate table with replacements for the Api\Preferences
- *
- */
- public function show_replacements()
- {
- $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();
-
- echo "