diff --git a/include/ajax.content.php b/include/ajax.content.php index 9ea0d7d1431bdf58aac6b2ffada47acb7e4585bd..8737473570d93aae7ba826f156deb66a031e3941 100644 --- a/include/ajax.content.php +++ b/include/ajax.content.php @@ -199,5 +199,43 @@ class ContentAjaxAPI extends AjaxController { $errors = Format::htmlchars($errors); include STAFFINC_DIR . 'templates/content-manage.tmpl.php'; } + + function context() { + global $thisstaff; + + if (!$thisstaff) + Http::response(403, 'Login Required'); + if (!$_GET['root']) + Http::response(400, '`root` is required parameter'); + + // Get the template for this template + $tpl_info = EmailTemplateGroup::getTemplateDescription($_GET['root']); + if (!$tpl_info) + Http::response(422, 'No such context'); + + $global = osTicket::getVarScope(); + + $contextTypes = array( + 'assignee' => array('class' => 'Staff', 'desc' => 'Newly assigned agent'), + 'assigner' => array('class' => 'Staff', 'desc' => 'Agent performing the assignment'), + 'comments' => 'Agent supplied comments', + 'message' => array('class' => 'MessageThreadEntry', 'desc' => 'Message from the EndUser'), + 'note' => array('class' => 'NoteThreadEntry', 'desc' => 'Internal note'), + 'poster' => array('class' => 'User', 'desc' => 'EndUser or Agent originating the message'), + 'recipient' => array('class' => 'TicketUser', 'desc' => 'Message recipient'), + 'response' => array('class' => 'ResponseThreadEntry', 'desc' => 'Agent reply'), + 'signature' => 'Selected staff or department signature', + 'staff' => array('class' => 'Staff', 'desc' => 'Agent originating the activity'), + 'ticket' => array('class' => 'Ticket', 'desc' => 'The ticket'), + ); + $context = array(); + foreach ($tpl_info['context'] as $C) { + $context[$C] = $contextTypes[$C]; + } + $items = VariableReplacer::compileScope($context + $global); + + header('Content-Type: application/json'); + return $this->encode($items); + } } ?> diff --git a/include/class.client.php b/include/class.client.php index 25efc2ca60132537015107d13d273b913bc13ee7..5b819e2125857909a86ae27992b67b040cf23bbf 100644 --- a/include/class.client.php +++ b/include/class.client.php @@ -16,7 +16,7 @@ require_once INCLUDE_DIR.'class.user.php'; abstract class TicketUser -implements EmailContact, ITicketUser { +implements EmailContact, ITicketUser, TemplateVariable { static private $token_regex = '/^(?P<type>\w{1})(?P<algo>\d+)x(?P<hash>.*)$/i'; @@ -55,6 +55,13 @@ implements EmailContact, ITicketUser { } + static function getVarScope() { + return array( + 'name' => array('class' => 'PersonsName', 'desc' => __('Full name')), + 'ticket_link' => __('Link to the ticket'), + ); + } + function getId() { return ($this->user) ? $this->user->getId() : null; } function getEmail() { return ($this->user) ? $this->user->getEmail() : null; } diff --git a/include/class.company.php b/include/class.company.php index c9aa22f04af3889ea22b564f06ffdf2dff936a71..6773bd80e83ef59e724744fd9b33663c0f269b57 100644 --- a/include/class.company.php +++ b/include/class.company.php @@ -17,7 +17,8 @@ require_once(INCLUDE_DIR.'class.forms.php'); require_once(INCLUDE_DIR.'class.dynamic_forms.php'); -class Company { +class Company +implements TemplateVariable { var $form; var $entry; @@ -59,6 +60,12 @@ class Company { return $this->getName(); } + static function getVarScope() { + return VariableReplacer::compileFormScope( + DynamicForm::lookup(array('type'=>'C')) + ); + } + function __toString() { try { if ($name = $this->getForm()->getAnswer('name')) diff --git a/include/class.dept.php b/include/class.dept.php index 39f914f9f401233909742e991d528c7b6551c1c3..43a98692d1af69cf0c67e2dd3d90dac30d0741ae 100644 --- a/include/class.dept.php +++ b/include/class.dept.php @@ -14,7 +14,8 @@ vim: expandtab sw=4 ts=4 sts=4: **********************************************************************/ -class Dept extends VerySimpleModel { +class Dept extends VerySimpleModel +implements TemplateVariable { static $meta = array( 'table' => DEPT_TABLE, @@ -71,6 +72,26 @@ class Dept extends VerySimpleModel { return $this->getName(); } + static function getVarScope() { + return array( + 'name' => 'Department name', + 'manager' => array( + 'class' => 'Staff', 'desc' => 'Department manager', + 'exclude' => 'dept', + ), + 'members' => array( + 'class' => 'UserList', 'desc' => 'Department members', + ), + 'parent' => array( + 'class' => 'Dept', 'desc' => 'Parent department', + ), + 'sla' => array( + 'class' => 'SLA', 'desc' => 'Service Level Agreement', + ), + 'signature' => 'Department signature', + ); + } + function getId() { return $this->id; } @@ -170,7 +191,7 @@ class Dept extends VerySimpleModel { $this->_members = $members->all(); } - return $this->_members; + return new UserList($this->_members); } function getAvailableMembers() { diff --git a/include/class.list.php b/include/class.list.php index 841ff3ea5960b4c2394b2fcd595d61026c2d697f..5a1092ea1597b94880ce749335ce01e7a8934c8d 100644 --- a/include/class.list.php +++ b/include/class.list.php @@ -14,8 +14,8 @@ vim: expandtab sw=4 ts=4 sts=4: **********************************************************************/ - require_once(INCLUDE_DIR .'class.dynamic_forms.php'); +require_once(INCLUDE_DIR .'class.variable.php'); /** * Interface for Custom Lists @@ -939,7 +939,9 @@ class TicketStatusList extends CustomListHandler { } } -class TicketStatus extends VerySimpleModel implements CustomListItem { +class TicketStatus +extends VerySimpleModel +implements CustomListItem, TemplateVariable { static $meta = array( 'table' => TICKET_STATUS_TABLE, @@ -1195,6 +1197,15 @@ class TicketStatus extends VerySimpleModel implements CustomListItem { return $T != $tag ? $T : $default; } + // TemplateVariable interface + static function getVarScope() { + $base = array( + 'name' => __('Status label'), + 'state' => __('State name (e.g. open or closed)'), + ); + return $base; + } + function getConfiguration() { if (!$this->_settings) { diff --git a/include/class.organization.php b/include/class.organization.php index ffd25216f52026e82a41d0f4a9b105bd82f17a5c..16ba01293ddc54c6699d06b0e1ff1d81735120c1 100644 --- a/include/class.organization.php +++ b/include/class.organization.php @@ -147,7 +147,8 @@ class OrganizationCdata extends VerySimpleModel { } -class Organization extends OrganizationModel { +class Organization extends OrganizationModel +implements TemplateVariable { var $_entries; var $_forms; @@ -299,6 +300,28 @@ class Organization extends OrganizationModel { foreach ($this->getDynamicData() as $e) if ($a = $e->getAnswer($tag)) return $a; + + switch ($tag) { + case 'members': + return new UserList($this->users); + case 'manager': + return $this->getAccountManager(); + case 'contacts': + return new UserList($this->users->filter(array( + 'flags__hasbit' => User::PRIMARY_ORG_CONTACT + ))); + } + } + + static function getVarScope() { + $base = array( + 'contacts' => array('class' => 'UserList', 'desc' => 'Primary contacts'), + 'manager' => 'Account manager', + 'members' => array('class' => 'UserList', 'desc' => 'Organization members'), + 'name' => 'Organization name', + ); + $extra = VariableReplacer::compileFormScope(OrganizationForm::getInstance()); + return $base + $extra; } function update($vars, &$errors) { diff --git a/include/class.osticket.php b/include/class.osticket.php index 15500c232ddbc3920fb0c5b73848d4c9e6ebebbc..75a247c1072a7cf3143c8929a4ef46e13b228d1b 100644 --- a/include/class.osticket.php +++ b/include/class.osticket.php @@ -145,6 +145,13 @@ class osTicket { return $replacer->replaceVars($input); } + static function getVarScope() { + return array( + 'url' => __("osTicket's base url (FQDN)"), + 'company' => array('class' => 'Company', 'desc' => __('Company Information')), + ); + } + function addExtraHeader($header, $pjax_script=false) { $this->headers[md5($header)] = $header; $this->pjax_extra[md5($header)] = $pjax_script; diff --git a/include/class.priority.php b/include/class.priority.php index ac6976d29bb53517b41ee82d5dc4bf536e124bf0..68f1c39d6cc2cbab3eddf6957fcd9aed1087849d 100644 --- a/include/class.priority.php +++ b/include/class.priority.php @@ -14,7 +14,8 @@ vim: expandtab sw=4 ts=4 sts=4: **********************************************************************/ -class Priority extends VerySimpleModel { +class Priority extends VerySimpleModel +implements TemplateVariable { static $meta = array( 'table' => PRIORITY_TABLE, @@ -46,6 +47,14 @@ class Priority extends VerySimpleModel { return $this->ispublic; } + // TemplateVariable interface + function asVar() { return $this->getDesc(); } + static function getVarScope() { + return array( + 'desc' => 'Priority description', + ); + } + function __toString() { return $this->getDesc(); } diff --git a/include/class.sla.php b/include/class.sla.php index 8edb7a4360099b32506af1e563850e44887294a8..f26a399f648ffb00aeef9f0520e3e7ab3f1b992d 100644 --- a/include/class.sla.php +++ b/include/class.sla.php @@ -13,7 +13,8 @@ vim: expandtab sw=4 ts=4 sts=4: **********************************************************************/ -class SLA extends VerySimpleModel { +class SLA extends VerySimpleModel +implements TemplateVariable { static $meta = array( 'table' => SLA_TABLE, @@ -95,6 +96,18 @@ class SLA extends VerySimpleModel { return $T != $tag ? $T : $default; } + // TemplateVariable interface + function asVar() { + return $this->getName(); + } + + static function getVarScope() { + return array( + 'name' => 'SLA Name', + 'graceperiod' => 'Grace period (in hours)', + ); + } + function update($vars, &$errors) { if (!$vars['grace_period']) diff --git a/include/class.staff.php b/include/class.staff.php index 4a13e1b382104d893ad1aae63376667ca53ce48c..c2765915de5a401e0f297d42da121b149a047ce1 100644 --- a/include/class.staff.php +++ b/include/class.staff.php @@ -24,7 +24,7 @@ include_once(INCLUDE_DIR.'class.user.php'); include_once(INCLUDE_DIR.'class.auth.php'); class Staff extends VerySimpleModel -implements AuthenticatedUser, EmailContact { +implements AuthenticatedUser, EmailContact, TemplateVariable { static $meta = array( 'table' => STAFF_TABLE, @@ -78,6 +78,15 @@ implements AuthenticatedUser, EmailContact { return $this->__toString(); } + static function getVarScope() { + return array( + 'name' => array( + 'class' => 'PersonsName', 'desc' => 'Name of the agent', + ), + 'signature' => "Agent's signature", + ); + } + function getHashtable() { $base = $this->ht; $base['group'] = $base['group_id']; diff --git a/include/class.team.php b/include/class.team.php index 5a81170782a7225316d335b225ff3e0c2caeb622..1d4358bfab656339fa16b6e09152c38737a67c96 100644 --- a/include/class.team.php +++ b/include/class.team.php @@ -14,7 +14,8 @@ vim: expandtab sw=4 ts=4 sts=4: **********************************************************************/ -class Team extends VerySimpleModel { +class Team extends VerySimpleModel +implements TemplateVariable { static $meta = array( 'table' => TEAM_TABLE, @@ -42,6 +43,18 @@ class Team extends VerySimpleModel { return (string) $this->getName(); } + static function getVarScope() { + return array( + 'name' => 'Team name', + 'lead' => array( + 'class' => 'Staff', 'desc' => 'Team leader', + ), + 'members' => array( + 'class' => 'UserList', 'desc' => 'Team members', + ), + ); + } + function getId() { return $this->team_id; } @@ -62,7 +75,7 @@ class Team extends VerySimpleModel { $this->_members[] = $m->staff; } - return $this->_members; + return new UserList($this->_members); } function hasMember($staff) { diff --git a/include/class.template.php b/include/class.template.php index 00a81c066c486a9fe9f95c1fea17b3bae57c683e..c656e236ab6bf7d6b0b943908bd7ca13d9bff5dc 100644 --- a/include/class.template.php +++ b/include/class.template.php @@ -30,56 +30,108 @@ class EmailTemplateGroup { 'ticket.autoresp'=>array( 'group'=>'ticket.user', 'name'=>/* @trans */ 'New Ticket Auto-response', - 'desc'=>/* @trans */ 'Autoresponse sent to user, if enabled, on new ticket.'), + 'desc'=>/* @trans */ 'Autoresponse sent to user, if enabled, on new ticket.', + 'context' => array( + 'ticket', 'signature', 'message', 'recipient' + ), + ), 'ticket.autoreply'=>array( 'group'=>'ticket.user', 'name'=>/* @trans */ 'New Ticket Auto-reply', - 'desc'=>/* @trans */ 'Canned Auto-reply sent to user on new ticket, based on filter matches. Overwrites "normal" auto-response.'), + 'desc'=>/* @trans */ 'Canned Auto-reply sent to user on new ticket, based on filter matches. Overwrites "normal" auto-response.', + 'context' => array( + 'ticket', 'signature', 'response', 'recipient', + ), + ), 'message.autoresp'=>array( 'group'=>'ticket.user', 'name'=>/* @trans */ 'New Message Auto-response', - 'desc'=>/* @trans */ 'Confirmation sent to user when a new message is appended to an existing ticket.'), + 'desc'=>/* @trans */ 'Confirmation sent to user when a new message is appended to an existing ticket.', + 'context' => array( + 'ticket', 'signature', 'recipient', + ), + ), 'ticket.notice'=>array( 'group'=>'ticket.user', 'name'=>/* @trans */ 'New Ticket Notice', - 'desc'=>/* @trans */ 'Notice sent to user, if enabled, on new ticket created by an agent on their behalf (e.g phone calls).'), + 'desc'=>/* @trans */ 'Notice sent to user, if enabled, on new ticket created by an agent on their behalf (e.g phone calls).', + 'context' => array( + 'ticket', 'signature', 'recipient', 'staff', 'message', + ), + ), 'ticket.overlimit'=>array( 'group'=>'ticket.user', 'name'=>/* @trans */ 'Over Limit Notice', - 'desc'=>/* @trans */ 'A one-time notice sent, if enabled, when user has reached the maximum allowed open tickets.'), + 'desc'=>/* @trans */ 'A one-time notice sent, if enabled, when user has reached the maximum allowed open tickets.', + 'context' => array( + 'ticket', 'signature', + ), + ), 'ticket.reply'=>array( 'group'=>'ticket.user', 'name'=>/* @trans */ 'Response/Reply Template', - 'desc'=>/* @trans */ 'Template used on ticket response/reply'), + 'desc'=>/* @trans */ 'Template used on ticket response/reply', + 'context' => array( + 'ticket', 'signature', 'response', 'staff', 'poster', 'recipient', + ), + ), 'ticket.activity.notice'=>array( 'group'=>'ticket.user', 'name'=>/* @trans */ 'New Activity Notice', - 'desc'=>/* @trans */ 'Template used to notify collaborators on ticket activity (e.g CC on reply)'), + 'desc'=>/* @trans */ 'Template used to notify collaborators on ticket activity (e.g CC on reply)', + 'context' => array( + 'ticket', 'signature', 'message', 'poster', 'recipient', + ), + ), 'ticket.alert'=>array( 'group'=>'ticket.staff', 'name'=>/* @trans */ 'New Ticket Alert', - 'desc'=>/* @trans */ 'Alert sent to agents, if enabled, on new ticket.'), + 'desc'=>/* @trans */ 'Alert sent to agents, if enabled, on new ticket.', + 'context' => array( + 'ticket', 'recipient', 'message', + ), + ), 'message.alert'=>array( 'group'=>'ticket.staff', 'name'=>/* @trans */ 'New Message Alert', - 'desc'=>/* @trans */ 'Alert sent to agents, if enabled, when user replies to an existing ticket.'), + 'desc'=>/* @trans */ 'Alert sent to agents, if enabled, when user replies to an existing ticket.', + 'context' => array( + 'ticket', 'recipient', 'message', 'poster', + ), + ), 'note.alert'=>array( 'group'=>'ticket.staff', 'name'=>/* @trans */ 'Internal Activity Alert', - 'desc'=>/* @trans */ 'Alert sent out to Agents when internal activity such as an internal note or an agent reply is appended to a ticket.'), + 'desc'=>/* @trans */ 'Alert sent out to Agents when internal activity such as an internal note or an agent reply is appended to a ticket.', + 'context' => array( + 'ticket', 'recipient', 'note', 'comments', 'activity', + ), + ), 'assigned.alert'=>array( 'group'=>'ticket.staff', 'name'=>/* @trans */ 'Ticket Assignment Alert', - 'desc'=>/* @trans */ 'Alert sent to agents on ticket assignment.'), + 'desc'=>/* @trans */ 'Alert sent to agents on ticket assignment.', + 'context' => array( + 'ticket', 'recipient', 'comments', 'assignee', 'assigner', + ), + ), 'transfer.alert'=>array( 'group'=>'ticket.staff', 'name'=>/* @trans */ 'Ticket Transfer Alert', - 'desc'=>/* @trans */ 'Alert sent to agents on ticket transfer.'), + 'desc'=>/* @trans */ 'Alert sent to agents on ticket transfer.', + 'context' => array( + 'ticket', 'recipient', 'comments', 'staff', + ), + ), 'ticket.overdue'=>array( 'group'=>'ticket.staff', 'name'=>/* @trans */ 'Overdue Ticket Alert', - 'desc'=>/* @trans */ 'Alert sent to agents on stale or overdue tickets.'), - ); + 'desc'=>/* @trans */ 'Alert sent to agents on stale or overdue tickets.', + 'context' => array( + 'ticket', 'recipient', 'comments', + ), + ), + ); function EmailTemplateGroup($id){ $this->id=0; @@ -157,7 +209,7 @@ class EmailTemplateGroup { return (db_query($sql) && db_affected_rows()); } - function getTemplateDescription($name) { + static function getTemplateDescription($name) { return static::$all_names[$name]; } diff --git a/include/class.thread.php b/include/class.thread.php index 3cf9308025ffed40b4ca3744dd538c16d9e4715f..b5d8bdbd4ee23b81b7767274e42c9d46120e813f 100644 --- a/include/class.thread.php +++ b/include/class.thread.php @@ -394,7 +394,8 @@ class ThreadEntryEmailInfo extends VerySimpleModel { ); } -class ThreadEntry extends VerySimpleModel { +class ThreadEntry extends VerySimpleModel +implements TemplateVariable { static $meta = array( 'table' => THREAD_ENTRY_TABLE, 'pk' => array('id'), @@ -844,6 +845,7 @@ class ThreadEntry extends VerySimpleModel { return (string) $this->getBody(); } + // TemplateVariable interface function asVar() { return (string) $this->getBody()->display('email'); } @@ -865,6 +867,22 @@ class ThreadEntry extends VerySimpleModel { return false; } + static function getVarScope() { + return array( + 'body' => 'Formatted message body', + 'create_date' => 'Date created', + 'ip_address' => 'IP address of remote user, for web submissions', + 'poster' => 'Name of the thread item originator', + 'staff' => array( + 'class' => 'Staff', 'desc' => 'Agent posting the note or response', + ), + 'subject' => 'Subject of the message, if any', + 'user' => array( + 'class' => 'User', 'desc' => 'User posting the message', + ), + ); + } + /** * Parameters: * mailinfo (hash<String>) email header information. Must include keys @@ -1493,6 +1511,12 @@ class MessageThreadEntry extends ThreadEntry { return parent::add($vars); } + + static function getVarScope() { + $base = parent::getVarScope(); + unset($base['staff']); + return $base; + } } /* thread entry of type response */ @@ -1533,6 +1557,12 @@ class ResponseThreadEntry extends ThreadEntry { return parent::add($vars); } + + static function getVarScope() { + $base = parent::getVarScope(); + unset($base['user']); + return $base; + } } /* Thread entry of type note (Internal Note) */ @@ -1563,10 +1593,17 @@ class NoteThreadEntry extends ThreadEntry { return parent::add($vars); } + + static function getVarScope() { + $base = parent::getVarScope(); + unset($base['user']); + return $base; + } } // Object specific thread utils. -class ObjectThread extends Thread { +class ObjectThread extends Thread +implements TemplateVariable { private $_entries = array(); static $types = array( @@ -1694,6 +1731,13 @@ class ObjectThread extends Thread { } } + static function getVarScope() { + return array( + 'original' => array('class' => 'MessageThreadEntry', 'desc' => __('Original Message')), + 'lastmessage' => array('class' => 'MessageThreadEntry', 'desc' => __('Last Message')), + ); + } + static function lookup($criteria, $type=false) { if (!$type) return parent::lookup($criteria); diff --git a/include/class.ticket.php b/include/class.ticket.php index 30a5df7fd556d85d55074691e093289992d5aeab..c13e85fbb3d6fd2d0193f7bd0f6deaedd1cd3882 100644 --- a/include/class.ticket.php +++ b/include/class.ticket.php @@ -212,7 +212,7 @@ TicketCData::$meta['table'] = TABLE_PREFIX . 'ticket__cdata'; class Ticket -implements RestrictedAccess, Threadable { +implements RestrictedAccess, Threadable, TemplateVariable { var $id; var $number; @@ -1797,7 +1797,7 @@ implements RestrictedAccess, Threadable { } - //ticket obj as variable = ticket number. + // TemplateVariable interface function asVar() { return $this->getNumber(); } @@ -1854,6 +1854,52 @@ implements RestrictedAccess, Threadable { return false; } + static function getVarScope() { + $base = array( + 'assigned' => 'Assigned agent and/or team', + 'close_date' => 'Date of ticket closure', + 'create_date' => 'Ticket create date', + 'dept' => array( + 'class' => 'Dept', 'desc' => 'Department', + ), + 'due_date' => 'Ticket due date', + 'email' => 'Default email address of ticket owner', + 'name' => array( + 'class' => 'PersonsName', 'desc' => __('Name of ticket owner'), + ), + 'number' => 'Ticket number', + 'phone' => 'Phone number of ticket owner', + 'priority' => array( + 'class' => 'Priority', 'desc' => __('Ticket priority'), + ), + 'recipients' => array( + 'class' => 'UserList', 'desc' => 'Ticket participant list', + ), + 'status' => array( + 'class' => 'TicketStatus', 'desc' => __('Ticket status'), + ), + 'staff' => array( + 'class' => 'Staff', 'desc' => __('Assigned/closing agent'), + ), + 'subject' => 'Subject', + 'team' => array( + 'class' => 'Team', 'desc' => __('Assigned/closing team'), + ), + 'thread' => array( + 'class' => 'TicketThread', 'desc' => 'Ticket thread', + ), + 'topic' => array( + 'class' => 'Topic', 'desc' => 'Help topic', + ), + 'user' => array( + 'class' => 'User', 'desc' => __('Ticket owner'), + ), + ); + + $extra = VariableReplacer::compileFormScope(TicketForm::getInstance()); + return $base + $extra; + } + //Replace base variables. function replaceVars($input, $vars = array()) { global $ost; diff --git a/include/class.topic.php b/include/class.topic.php index 9daeb7f07dd33b1e6493d4f85dc1b0b062ce834d..c60aa2c9e2695407deb58bc226ec261cec2e7bb7 100644 --- a/include/class.topic.php +++ b/include/class.topic.php @@ -17,7 +17,8 @@ require_once INCLUDE_DIR . 'class.sequence.php'; require_once INCLUDE_DIR . 'class.filter.php'; -class Topic extends VerySimpleModel { +class Topic extends VerySimpleModel +implements TemplateVariable { static $meta = array( 'table' => TOPIC_TABLE, @@ -72,6 +73,22 @@ class Topic extends VerySimpleModel { return $this->getName(); } + static function getVarScope() { + return array( + 'dept' => array( + 'class' => 'Dept', 'desc' => 'Department', + ), + 'fullname' => 'Help topic full path', + 'name' => 'Help topic name', + 'parent' => array( + 'class' => 'Topic', 'desc' => 'Parent help topic', + ), + 'sla' => array( + 'class' => 'SLA', 'desc' => 'Service Level Agreement', + ), + ); + } + function getId() { return $this->topic_id; } diff --git a/include/class.user.php b/include/class.user.php index 7cb3cf2dfb9ef1855e56c6269a1f125831c3ae4b..5c9eee9f952dd3823dbd15de24d1c11ed9f50766 100644 --- a/include/class.user.php +++ b/include/class.user.php @@ -17,6 +17,7 @@ require_once INCLUDE_DIR . 'class.orm.php'; require_once INCLUDE_DIR . 'class.util.php'; require_once INCLUDE_DIR . 'class.organization.php'; +require_once INCLUDE_DIR . 'class.variable.php'; class UserEmailModel extends VerySimpleModel { static $meta = array( @@ -188,7 +189,8 @@ class UserCdata extends VerySimpleModel { } } -class User extends UserModel { +class User extends UserModel +implements TemplateVariable { var $_entries; var $_forms; @@ -331,6 +333,18 @@ class User extends UserModel { return $a; } + static function getVarScope() { + $base = array( + 'email' => 'Default email address', + 'name' => array( + 'class' => 'PersonsName', 'desc' => 'User name, default format' + ), + 'organization' => array('class' => 'Organization', 'desc' => 'Organization'), + ); + $extra = VariableReplacer::compileFormScope(UserForm::getInstance()); + return $base + $extra; + } + function addDynamicData($data) { return $this->addForm(UserForm::objects()->one(), 1, $data); } @@ -642,7 +656,8 @@ class User extends UserModel { } } -class PersonsName { +class PersonsName +implements TemplateVariable { var $format; var $parts; var $name; @@ -761,6 +776,16 @@ class PersonsName { return $this->__toString(); } + static function getVarScope() { + $formats = array(); + foreach (static::$formats as $name=>$info) { + if (in_array($name, array('original', 'complete'))) + continue; + $formats[$name] = $info[0]; + } + return $formats; + } + function __toString() { @list(, $func) = static::$formats[$this->format]; @@ -1220,9 +1245,14 @@ class UserAccountStatus { /* * Generic user list. */ -class UserList extends ListObject { +class UserList extends ListObject +implements TemplateVariable { function __toString() { + return $this->getNames(); + } + + function getNames() { $list = array(); foreach($this->storage as $user) { @@ -1232,5 +1262,33 @@ class UserList extends ListObject { return $list ? implode(', ', $list) : ''; } + + function getFull() { + $list = array(); + foreach($this->storage as $user) { + if (is_object($user)) + $list[] = sprintf("%s <%s>", $user->getName(), $user->getEmail()); + } + + return $list ? implode(', ', $list) : ''; + } + + function getEmails() { + $list = array(); + foreach($this->storage as $user) { + if (is_object($user)) + $list[] = $user->getEmail(); + } + + return $list ? implode(', ', $list) : ''; + } + + static function getVarScope() { + return array( + 'names' => 'List of names', + 'emails' => 'List of email addresses', + 'full' => 'List of names and email addresses', + ); + } } ?> diff --git a/include/class.variable.php b/include/class.variable.php index ffb850ec8f264dd2e15ada81e37e90b268c0ccec..3186739ca4bc42dfc13a9712407d87d70c5a52bc 100644 --- a/include/class.variable.php +++ b/include/class.variable.php @@ -146,5 +146,69 @@ class VariableReplacer { return $vars; } + + static function compileScope($scope, $recurse=5, $exclude=false) { + $items = array(); + foreach ($scope as $name => $info) { + if ($exclude === $name) + continue; + if (isset($info['class']) && $recurse) { + $items[$name] = $info['desc']; + foreach (static::compileScope($info['class']::getVarScope(), $recurse-1, + @$info['exclude'] ?: $name) + as $name2=>$desc) { + $items["{$name}.{$name2}"] = $desc; + } + } + if (!is_array($info)) { + $items[$name] = $info; + } + } + return $items; + } + + static function compileFormScope($form) { + $items = array(); + foreach ($form->getFields() as $f) { + if (!($name = $f->get('name'))) + continue; + if (!$f->isStorable() || !$f->hasData()) + continue; + + $desc = $f->getLocal('label'); + $items[$name] = $desc; + foreach (VariableReplacer::compileFieldScope($f) as $name2=>$desc) { + $items["$name.$name2"] = $desc; + } + } + return $items; + } + + static function compileFieldScope($field, $recurse=2, $exclude=false) { + $items = array(); + if (!$field->hasSubFields()) + return $items; + + foreach ($field->getSubFields() as $f) { + if (!($name = $f->get('name'))) + continue; + if ($exclude === $name) + continue; + $items[$name] = $f->getLabel(); + if ($recurse) { + foreach (static::compileFieldScope($f, $recurse-1, $name) + as $name2=>$desc) { + $items["$name.$name2"] = $desc; + } + } + } + return $items; + } +} + +interface TemplateVariable { + // function asVar(); — not absolutely required + // function getVar($name); — not absolutely required + static function getVarScope(); } ?> diff --git a/include/staff/tpl.inc.php b/include/staff/tpl.inc.php index 37e29177e80d850efb54bbd6e8a0c79c891db421..cfbbe5790896adcd70b350dede508b42c96dde1d 100644 --- a/include/staff/tpl.inc.php +++ b/include/staff/tpl.inc.php @@ -108,6 +108,7 @@ $tpl=$msgtemplates[$selected]; </div> <input type="hidden" name="draft_id" value=""/> <textarea name="body" cols="21" rows="16" style="width:98%;" wrap="soft" + data-root-context="<?php echo $selected; ?>" data-toolbar-external="#toolbar" class="richtext draft" <?php list($draft, $attrs) = Draft::getDraftAndDataAttrs('tpl.'.$selected, $tpl_id, $info['body']); echo $attrs; ?>><?php echo $draft ?: $info['body']; diff --git a/js/redactor-osticket.js b/js/redactor-osticket.js index bd32edda61afbc875b2aa08d3fef296f6912db39..b908a2981d5bda79fddbd5f85f9dfcdd8b4bb4d7 100644 --- a/js/redactor-osticket.js +++ b/js/redactor-osticket.js @@ -294,6 +294,9 @@ $(function() { options['plugins'].push('imagepaste'); options.draftDelete = el.hasClass('draft-delete'); } + if (true || 'scp') { // XXX: Add this to SCP only + options['plugins'].push('contexttypeahead'); + } if (el.hasClass('fullscreen')) options['plugins'].push('fullscreen'); if ($('#ticket_thread[data-thread-id]').length) diff --git a/js/redactor-plugins.js b/js/redactor-plugins.js index f41fb48e9cd82e81e93fcbedb1d19a9fe20f2b26..16da1f7e3fa98e37d23d700004fb8e85048eb16d 100644 --- a/js/redactor-plugins.js +++ b/js/redactor-plugins.js @@ -1606,3 +1606,155 @@ RedactorPlugins.imageannotate = function() { } }; }; + +RedactorPlugins.contexttypeahead = function() { + return { + typeahead: false, + context: false, + + init: function() { + if (!this.$element.data('rootContext')) + return; + + this.opts.keyupCallback = this.contexttypeahead.watch.bind(this); + this.opts.keydownCallback = this.contexttypeahead.watch.bind(this); + this.$editor.on('click', this.contexttypeahead.watch.bind(this)); + }, + + watch: function(e) { + var current = this.selection.getCurrent(), + search = new RegExp(/%\{([^}]*)$/), + match; + + if (!current) + return; + + content = current.textContent; + if (e.which == 27 || !(match = search.exec(content))) + // No longer in a element — close typeahead + return this.contexttypeahead.destroy(); + + // Locate the position of the cursor and the number of characters back + // to the `%{` symbols + var sel = this.selection.get(), + range = this.sel.getRangeAt(0), + clientRects = range.getClientRects(), + position = clientRects[0], + cursorAt = range.endOffset, + backTextLen = match[1].length - content.length + cursorAt, + backText = match[1].substring(0, backTextLen); + + if (backTextLen < 0) + return this.contexttypeahead.destroy(); + + if (e.type == 'click') + return; + + // Insert a hidden text input to receive the typed text and add a + // typeahead widget + if (!this.contexttypeahead.typeahead) { + this.contexttypeahead.typeahead = $('<input type="text">') + .css({position: 'absolute', visibility: 'hidden'}) + .width(0).height(position.height) + .appendTo(document.body) + .typeahead({ + property: 'variable', + minLength: 0, + highlighter: function(variable, item) { + var base = $.fn.typeahead.Constructor.prototype.highlighter.call(this, variable); + return base + $('<span class="faded"/>') + .text(' — ' + item.desc) + .wrap('<div>').parent().html(); + }, + source: this.contexttypeahead.getContext.bind(this), + sorter: function(items) { + items.sort( + function(a,b) {return a.variable > b.variable ? 1 : -1;} + ); + return items; + }, + matcher: function(item) { + if (item.toLowerCase().indexOf(this.query.toLowerCase()) !== 0) + return false; + + return (this.query.match(/\./g) || []).length == (item.match(/\./g) || []).length; + }, + onselect: this.contexttypeahead.select.bind(this) + }); + } + + var left = position.left - this.contexttypeahead.textWidth( + backText, + this.selection.getParent() || $('<div class="redactor-editor">') + ); + + this.contexttypeahead.typeahead + .val(match[1]) + .trigger(e) + .css({top: position.top + $(window).scrollTop(), left: left}); + + return !e.isDefaultPrevented(); + }, + + getContext: function(typeahead, query) { + var dfd, + root = this.$element.data('rootContext'); + if (!this.contexttypeahead.context) { + dfd = $.Deferred(); + $.ajax('ajax.php/content/context', { + data: {root: root}, + success: function(json) { + var items = $.map(json, function(v,k) { + return {variable: k, desc: v}; + }); + dfd.resolve(items); + } + }); + this.contexttypeahead.context = dfd; + } + // Only fetch the context once for this redactor box + this.contexttypeahead.context.then(function(items) { + typeahead.process(items); + }); + }, + + textWidth: function(text, clone) { + var c = $(clone), + o = c.clone().text(text) + .css({'position': 'absolute', 'float': 'left', 'white-space': 'nowrap', 'visibility': 'hidden'}) + .css({'font-family': c.css('font-family'), 'font-weight': c.css('font-weight'), + 'font-size': c.css('font-size')}) + .appendTo($('body')), + w = o.width(); + + o.remove(); + + return w; + }, + + destroy: function() { + if (this.contexttypeahead.typeahead) { + this.contexttypeahead.typeahead.typeahead('hide'); + this.contexttypeahead.typeahead.remove(); + this.contexttypeahead.typeahead = false; + } + // TODO: Hide typeahead widget + }, + + select: function(item) { + var current = this.selection.getCurrent(), + search = new RegExp(/%\{([^}]*)$/); + + if (!current) + return; + + // Set cursor at the end of the expanded text + var q = current.textContent + = current.textContent.replace(search, '%{' + item.variable); + this.range.setStart(current, current.length); + this.range.setEnd(current, current.length); + this.selection.addRange(); + return this.contexttypeahead.destroy(); + } + }; +}; diff --git a/scp/ajax.php b/scp/ajax.php index 7a6b95fcb3662333969698cfc58ff0527f82b5e7..f34bcfb930f912ea70e6b4592324924ec62fd4a7 100644 --- a/scp/ajax.php +++ b/scp/ajax.php @@ -41,6 +41,7 @@ $dispatcher = patterns('', )), url('^/content/', patterns('ajax.content.php:ContentAjaxAPI', url_get('^log/(?P<id>\d+)', 'log'), + url_get('^context$', 'context'), url_get('^ticket_variables', 'ticket_variables'), url_get('^signature/(?P<type>\w+)(?:/(?P<id>\d+))?$', 'getSignature'), url_get('^(?P<id>\d+)/(?:(?P<lang>\w+)/)?manage$', 'manageContent'), diff --git a/scp/css/scp.css b/scp/css/scp.css index 64735d3de27b7959e24fe40d1179aa2aff0764e1..ca4e66cf37b5bce4ebaf87f5e21d8e668d9625eb 100644 --- a/scp/css/scp.css +++ b/scp/css/scp.css @@ -56,7 +56,8 @@ div#header a { } .faded { - color:#666; + color: #666; + color: rgba(0,0,0,0.5); } .faded-more { color: #aaa; diff --git a/scp/css/typeahead.css b/scp/css/typeahead.css index 981923ab1f4c200b7121f5563171101dfea0a4d2..80f67886d7ba4f3ef32f8366198efe04cb901410 100644 --- a/scp/css/typeahead.css +++ b/scp/css/typeahead.css @@ -56,3 +56,12 @@ text-decoration: none; background-color: #0088cc; } +.dropdown-menu li > a:hover .faded, +.dropdown-menu .active > a .faded, +.dropdown-menu .active > a:hover .faded { + color: rgba(255,255,255,0.6); +} + +.dropdown-menu li + li { + border-top: 1px solid rgba(0,0,0,0.15); +}