diff --git a/bootstrap.php b/bootstrap.php
index d2afa2243f7bfa2914da15428c3f8e2c9b197fcb..2085a4fbc1909d4c00ab79424e405f4c6ebd78b0 100644
--- a/bootstrap.php
+++ b/bootstrap.php
@@ -116,6 +116,7 @@ class Bootstrap {
         define('FORM_ANSWER_TABLE',$prefix.'form_entry_values');
 
         define('TOPIC_TABLE',$prefix.'help_topic');
+        define('TOPIC_FORM_TABLE',$prefix.'help_topic_form');
         define('SLA_TABLE', $prefix.'sla');
 
         define('EMAIL_TABLE',$prefix.'email');
@@ -124,6 +125,7 @@ class Bootstrap {
 
         define('FILTER_TABLE', $prefix.'filter');
         define('FILTER_RULE_TABLE', $prefix.'filter_rule');
+        define('FILTER_ACTION_TABLE', $prefix.'filter_action');
 
         define('PLUGIN_TABLE', $prefix.'plugin');
         define('SEQUENCE_TABLE', $prefix.'sequence');
diff --git a/include/ajax.filter.php b/include/ajax.filter.php
new file mode 100644
index 0000000000000000000000000000000000000000..5ef5ec82cc95fbec05d727dfdae8ba071ba32af3
--- /dev/null
+++ b/include/ajax.filter.php
@@ -0,0 +1,28 @@
+<?php
+
+require_once(INCLUDE_DIR . 'class.filter.php');
+
+class FilterAjaxAPI extends AjaxController {
+
+    function getFilterActionForm($type) {
+        if (!($A = FilterAction::lookupByType($type)))
+            Http::response(404, 'No such filter action type');
+
+        $form = $A->getConfigurationForm();
+        ?>
+        <div style="position:relative">
+            <div class="pull-right" style="position:absolute;top:2px;right:2px;">
+                <a href="#" title="<?php echo __('clear'); ?>" onclick="javascript:
+        if (!confirm(__('You sure?')))
+            return false;
+        $(this).closest('tr').fadeOut(400, function() { $(this).hide(); });
+        return false;"><i class="icon-trash"></i></a>
+            </div>
+        <?php
+        include STAFFINC_DIR . 'templates/dynamic-form-simple.tmpl.php';
+        ?>
+        </div>
+        <?php
+    }
+
+}
diff --git a/include/ajax.forms.php b/include/ajax.forms.php
index 6c35f1bb2fe2055d220c89c583261a189223ddc0..169290c945bac96986ef2ef222710691256851b5 100644
--- a/include/ajax.forms.php
+++ b/include/ajax.forms.php
@@ -24,13 +24,13 @@ class DynamicFormsAjaxAPI extends AjaxController {
             $_SESSION[':form-data'] = array_merge($_SESSION[':form-data'], $_GET);
         }
 
-        if ($form = $topic->getForm()) {
+        foreach ($topic->getForms() as $form) {
             ob_start();
             $form->getForm($_SESSION[':form-data'])->render(!$client);
-            $html = ob_get_clean();
+            $html .= ob_get_clean();
             ob_start();
             print $form->getMedia();
-            $media = ob_get_clean();
+            $media .= ob_get_clean();
         }
         return $this->encode(array(
             'media' => $media,
@@ -140,5 +140,23 @@ class DynamicFormsAjaxAPI extends AjaxController {
             array('id'=>$field->ajaxUpload(true))
         );
     }
+
+    function getAllFields($id) {
+        global $thisstaff;
+
+        if (!$thisstaff)
+            Http::response(403, 'Login required');
+        elseif (!$form = DynamicForm::lookup($id))
+            Http::response(400, 'No such form');
+
+        ob_start();
+        include STAFFINC_DIR . 'templates/dynamic-form-fields-view.tmpl.php';
+        $html = ob_get_clean();
+
+        return $this->encode(array(
+            'success'=>true,
+            'html' => $html,
+        ));
+    }
 }
 ?>
diff --git a/include/class.banlist.php b/include/class.banlist.php
index 939ae179b1cc142950daf65cd4c25f301c99b651..ae7b894479f4e3c56c51c30487bf1cd8fc16e2c2 100644
--- a/include/class.banlist.php
+++ b/include/class.banlist.php
@@ -16,15 +16,15 @@
 
 require_once "class.filter.php";
 class Banlist {
-    
+
     function add($email,$submitter='') {
         return self::getSystemBanList()->addRule('email','equal',$email);
     }
-    
+
     function remove($email) {
         return self::getSystemBanList()->removeRule('email','equal',$email);
     }
-    
+
     function isbanned($email) {
         return TicketFilter::isBanned($email);
     }
@@ -49,7 +49,9 @@ class Banlist {
             'name'          => 'SYSTEM BAN LIST',
             'isactive'      => 1,
             'match_all_rules' => false,
-            'reject_ticket'  => true,
+            'actions'       => array(
+                'Nreject',
+            ),
             'rules'         => array(),
             'notes'         => __('Internal list for email banning. Do not remove')
         ), $errors);
diff --git a/include/class.dynamic_forms.php b/include/class.dynamic_forms.php
index 181c1bdbd524bc8c3c2582c874b03ca127738ba9..e620669c9339a1dcfe1f5f31e56b530f14c0909a 100644
--- a/include/class.dynamic_forms.php
+++ b/include/class.dynamic_forms.php
@@ -32,6 +32,11 @@ class DynamicForm extends VerySimpleModel {
         'table' => FORM_SEC_TABLE,
         'ordering' => array('title'),
         'pk' => array('id'),
+        'joins' => array(
+            'fields' => array(
+                'reverse' => 'DynamicFormField.form',
+            ),
+        ),
     );
 
     // Registered form types
@@ -96,8 +101,8 @@ class DynamicForm extends VerySimpleModel {
     }
 
 
-    function getTitle() { return $this->get('title'); }
-    function getInstructions() { return $this->get('instructions'); }
+    function getTitle() { return $this->getLocal('title'); }
+    function getInstructions() { return $this->getLocal('instructions'); }
 
     function getForm($source=false) {
         if (!$this->_form || $source) {
@@ -112,6 +117,14 @@ class DynamicForm extends VerySimpleModel {
         return $this->get('deletable');
     }
 
+    function disableFields(array $ids) {
+        foreach ($this->getFields() as $F) {
+            if (in_array($F->get('id'), $ids)) {
+                $F->disable();
+            }
+        }
+    }
+
     function instanciate($sort=1) {
         return DynamicFormEntry::create(array(
             'form_id'=>$this->get('id'), 'sort'=>$sort));
@@ -138,7 +151,9 @@ class DynamicForm extends VerySimpleModel {
             $this->set('updated', new SqlFunction('NOW'));
         if (isset($this->dirty['notes']))
             $this->notes = Format::sanitize($this->notes);
-        return parent::save($refetch);
+        if ($rv = parent::save($refetch | $this->dirty))
+            return $this->saveTranslations();
+        return $rv;
     }
 
     function delete() {
@@ -182,6 +197,53 @@ class DynamicForm extends VerySimpleModel {
         return $inst;
     }
 
+    function saveTranslations($vars=false) {
+        global $thisstaff;
+
+        $vars = $vars ?: $_POST;
+        $tags = array(
+            'title' => $this->getTranslateTag('title'),
+            'instructions' => $this->getTranslateTag('instructions'),
+        );
+        $rtags = array_flip($tags);
+        $translations = CustomDataTranslation::allTranslations($tags, 'phrase');
+        foreach ($translations as $t) {
+            $T = $rtags[$t->object_hash];
+            $content = @$vars['trans'][$t->lang][$T];
+            if (!isset($content))
+                continue;
+
+            // Content is not new and shouldn't be added below
+            unset($vars['trans'][$t->lang][$T]);
+
+            $t->text = $content;
+            $t->agent_id = $thisstaff->getId();
+            $t->updated = SqlFunction::NOW();
+            if (!$t->save())
+                return false;
+        }
+        // New translations (?)
+        foreach ($vars['trans'] as $lang=>$parts) {
+            if (!Internationalization::isLanguageInstalled($lang))
+                continue;
+            foreach ($parts as $T => $content) {
+                $content = trim($content);
+                if (!$content)
+                    continue;
+                $t = CustomDataTranslation::create(array(
+                    'type'      => 'phrase',
+                    'object_hash' => $tags[$T],
+                    'lang'      => $lang,
+                    'text'      => $content,
+                    'agent_id'  => $thisstaff->getId(),
+                    'updated'   => SqlFunction::NOW(),
+                ));
+                if (!$t->save())
+                    return false;
+            }
+        }
+        return true;
+    }
 
 
     static function getCrossTabQuery($object_type, $object_id='object_id', $exclude=array()) {
@@ -422,6 +484,7 @@ class DynamicFormField extends VerySimpleModel {
     );
 
     var $_field;
+    var $_disabled = false;
 
     const FLAG_ENABLED          = 0x00001;
     const FLAG_EXT_STORED       = 0x00002; // Value stored outside of form_entry_value
@@ -525,6 +588,12 @@ class DynamicFormField extends VerySimpleModel {
     function  isEditable() {
         return $this->hasFlag(self::FLAG_MASK_EDIT);
     }
+    function disable() {
+        $this->_disabled = true;
+    }
+    function isEnabled() {
+        return !$this->_disabled && $this->hasFlag(self::FLAG_ENABLED);
+    }
 
     function hasFlag($flag) {
         return (isset($this->flags) && ($this->flags & $flag) != 0);
@@ -638,19 +707,19 @@ class DynamicFormField extends VerySimpleModel {
         return $this->hasFlag(self::FLAG_CLOSE_REQUIRED);
     }
     function isEditableToStaff() {
-        return $this->hasFlag(self::FLAG_ENABLED)
+        return $this->isEnabled()
             && $this->hasFlag(self::FLAG_AGENT_EDIT);
     }
     function isVisibleToStaff() {
-        return $this->hasFlag(self::FLAG_ENABLED)
+        return $this->isEnabled()
             && $this->hasFlag(self::FLAG_AGENT_VIEW);
     }
     function isEditableToUsers() {
-        return $this->hasFlag(self::FLAG_ENABLED)
+        return $this->isEnabled()
             && $this->hasFlag(self::FLAG_CLIENT_EDIT);
     }
     function isVisibleToUsers() {
-        return $this->hasFlag(self::FLAG_ENABLED)
+        return $this->isEnabled()
             && $this->hasFlag(self::FLAG_CLIENT_VIEW);
     }
 
@@ -689,10 +758,10 @@ class DynamicFormField extends VerySimpleModel {
         $this->save();
     }
 
-    function save() {
+    function save($refetch=false) {
         if (count($this->dirty))
             $this->set('updated', new SqlFunction('NOW'));
-        return parent::save();
+        return parent::save($this->dirty || $refetch);
     }
 
     static function create($ht=false) {
@@ -722,7 +791,7 @@ class DynamicFormEntry extends VerySimpleModel {
         'pk' => array('id'),
         'select_related' => array('form'),
         'fields' => array('id', 'form_id', 'object_type', 'object_id',
-            'sort', 'updated', 'created'),
+            'sort', 'extra', 'updated', 'created'),
         'joins' => array(
             'form' => array(
                 'null' => true,
@@ -785,6 +854,10 @@ class DynamicFormEntry extends VerySimpleModel {
             $this->_form = DynamicForm::lookup($this->get('form_id'));
             if ($this->_form && isset($this->id))
                 $this->_form->data($this);
+            if (isset($this->extra)) {
+                $x = JsonDataParser::decode($this->extra) ?: array();
+                $this->_form->disableFields($x['disable'] ?: array());
+            }
         }
         return $this->_form;
     }
@@ -958,6 +1031,7 @@ class DynamicFormEntry extends VerySimpleModel {
                 }
             }
             if (!$found && ($fImpl = $field->getImpl($field))
+                    && $field->isEnabled()
                     && !$fImpl->isPresentationOnly()) {
                 $a = DynamicFormEntryAnswer::create(
                     array('field_id'=>$field->get('id'), 'entry_id'=>$this->id));
diff --git a/include/class.email.php b/include/class.email.php
index ab0727f7cbf4c5cf941ebbf1893fc6873f6eb79f..31c3dda775dd5f0b8aef0c6300e3a03f6f088add 100644
--- a/include/class.email.php
+++ b/include/class.email.php
@@ -59,6 +59,19 @@ class EmailModel extends VerySimpleModel {
     static function getPermissions() {
         return self::$perms;
     }
+
+    static function getAddresses($options=array()) {
+        $objects = static::objects();
+        if ($options['smtp'])
+            $objects = $objects->filter(array('smtp_active'=>true));
+
+        $addresses = array();
+        foreach ($objects->values_flat('email_id', 'email') as $row) {
+            list($id, $email) = $row;
+            $addresses[$id] = $email;
+        }
+        return $addresses;
+    }
 }
 
 RolePermission::register(/* @trans */ 'Miscellaneous', EmailModel::getPermissions());
diff --git a/include/class.filter.php b/include/class.filter.php
index acc3a3590a7921d1bee8599417c20fd1714e011a..7a2ced7e2d7012b0980ba0940ea5a6d2e03f7c38 100644
--- a/include/class.filter.php
+++ b/include/class.filter.php
@@ -14,6 +14,8 @@
     vim: expandtab sw=4 ts=4 sts=4:
 **********************************************************************/
 
+require_once INCLUDE_DIR . 'class.filter_action.php';
+
 class Filter {
 
     var $id;
@@ -293,47 +295,28 @@ class Filter {
 
         return $match;
     }
+
+    function getActions() {
+        return FilterAction::objects()->filter(array(
+            'filter_id'=>$this->getId()
+        ));
+    }
     /**
      * If the matches() method returns TRUE, send the initial ticket to this
      * method to apply the filter actions defined
      */
-    function apply(&$ticket, $info=null) {
-        # TODO: Disable alerting
-        # XXX: Does this imply turning it on as well? (via ->sendAlerts())
-        if ($this->disableAlerts()) $ticket['autorespond']=false;
-        #       Set owning department (?)
-        if ($this->getDeptId())     $ticket['deptId']=$this->getDeptId();
-        #       Set ticket priority (?)
-        if ($this->getPriorityId()) $ticket['priorityId']=$this->getPriorityId();
-        #       Set SLA plan (?)
-        if ($this->getSLAId())      $ticket['slaId']=$this->getSLAId();
-        #       Set status
-        if ($this->getStatusId())   $ticket['statusId']=$this->getStatusId();
-        #       Auto-assign to (?)
-        #       XXX: Unset the other (of staffId or teamId) (?)
-        if ($this->getStaffId())    $ticket['staffId']=$this->getStaffId();
-        elseif ($this->getTeamId()) $ticket['teamId']=$this->getTeamId();
-        #       Override name with reply-to information from the TicketFilter
-        #       match
-        if ($this->useReplyToEmail() && $info['reply-to']) {
-            $changed = $info['reply-to'] != $ticket['email']
-                || ($info['reply-to-name'] && $ticket['name'] != $info['reply-to-name']);
-            $ticket['email'] = $info['reply-to'];
-            if ($info['reply-to-name'])
-                $ticket['name'] = $info['reply-to-name'];
-            if ($changed)
-                throw new FilterDataChanged();
+    function apply(&$ticket, $vars) {
+        foreach ($this->getActions() as $a) {
+            $a->setFilter($this);
+            $a->apply($ticket, $vars);
         }
+    }
 
-        # Use canned response.
-        if ($this->getCannedResponse())
-            $ticket['cannedResponseId'] = $this->getCannedResponse();
-
-        # Apply help topic
-        if ($this->getHelpTopic())
-            $ticket['topicId'] = $this->getHelpTopic();
+    function getVars() {
+        return $this->vars;
     }
-     static function getSupportedMatches() {
+
+    static function getSupportedMatches() {
         foreach (static::$match_types as $k=>&$v) {
             if (is_callable($v[0]))
                 $v[0] = $v[0]();
@@ -518,17 +501,9 @@ class Filter {
             .',name='.db_input($vars['name'])
             .',execorder='.db_input($vars['execorder'])
             .',email_id='.db_input($emailId)
-            .',dept_id='.db_input($vars['dept_id'])
-            .',status_id='.db_input($vars['status_id'])
-            .',priority_id='.db_input($vars['priority_id'])
-            .',sla_id='.db_input($vars['sla_id'])
-            .',topic_id='.db_input($vars['topic_id'])
             .',match_all_rules='.db_input($vars['match_all_rules'])
             .',stop_onmatch='.db_input(isset($vars['stop_onmatch'])?1:0)
             .',reject_ticket='.db_input(isset($vars['reject_ticket'])?1:0)
-            .',use_replyto_email='.db_input(isset($vars['use_replyto_email'])?1:0)
-            .',disable_autoresponder='.db_input(isset($vars['disable_autoresponder'])?1:0)
-            .',canned_response_id='.db_input($vars['canned_response_id'])
             .',notes='.db_input(Format::sanitize($vars['notes']));
 
 
@@ -558,8 +533,40 @@ class Filter {
         # Don't care about errors stashed in $xerrors
         $xerrors = array();
         self::save_rules($id,$vars,$xerrors);
-
-        return true;
+        self::save_actions($id, $vars, $errors);
+
+        return count($errors) == 0;
+    }
+
+    function save_actions($id, $vars, &$errors) {
+        if (!is_array(@$vars['actions']))
+            return;
+
+        foreach ($vars['actions'] as $sort=>$v) {
+            $info = substr($v, 1);
+            switch ($v[0]) {
+            case 'N': # new filter action
+                $I = FilterAction::create(array(
+                    'type'=>$info,
+                    'filter_id'=>$id,
+                    'sort' => (int) $sort,
+                ));
+                $I->setConfiguration($errors, $vars);
+                $I->save();
+                break;
+            case 'I': # exiting filter action
+                if ($I = FilterAction::lookup($info)) {
+                    $I->setConfiguration($errors, $vars);
+                    $I->sort = (int) $sort;
+                    $I->save();
+                }
+                break;
+            case 'D': # deleted filter action
+                if ($I = FilterAction::lookup($info))
+                    $I->delete();
+                break;
+            }
+        }
     }
 }
 
@@ -779,8 +786,6 @@ class TicketFilter {
      */
     function apply(&$ticket) {
         foreach ($this->getMatchingFilterList() as $filter) {
-            if ($filter->rejectOnMatch())
-                throw new RejectedException($filter, $ticket);
             $filter->apply($ticket, $this->vars);
             if ($filter->stopOnMatch()) break;
         }
diff --git a/include/class.filter_action.php b/include/class.filter_action.php
new file mode 100644
index 0000000000000000000000000000000000000000..f2367bc77b1f63e5a1d5eeb11a6d7b35af3b2013
--- /dev/null
+++ b/include/class.filter_action.php
@@ -0,0 +1,513 @@
+<?php
+
+require_once INCLUDE_DIR . 'class.orm.php';
+
+class FilterAction extends VerySimpleModel {
+    static $meta = array(
+        'table' => FILTER_ACTION_TABLE,
+        'pk' => array('id'),
+        'ordering' => array('sort'),
+    );
+
+    static $registry = array();
+    static $registry_group = array();
+
+    var $_impl;
+    var $_config;
+    var $_filter;
+
+    function getId() {
+        return $this->id;
+    }
+
+    function setFilter($filter) {
+        $this->_filter = $filter;
+    }
+    function getFilter() {
+        return $this->_filter;
+    }
+
+    function getConfiguration() {
+        if (!$this->_config) {
+            $this->_config = $this->get('configuration');
+            if (is_string($this->_config))
+                $this->_config = JsonDataParser::parse($this->_config);
+            elseif (!$this->_config)
+                $this->_config = array();
+            foreach ($this->getImpl()->getConfigurationOptions() as $name=>$field)
+                if (!isset($this->_config[$name]))
+                    $this->_config[$name] = $field->get('default');
+        }
+        return $this->_config;
+    }
+
+    function setConfiguration(&$errors=array(), $source=false) {
+        $config = array();
+        foreach ($this->getImpl()->getConfigurationForm($source ?: $_POST)
+                ->getFields() as $name=>$field) {
+            if (!$field->hasData())
+                continue;
+            $config[$name] = $field->to_php($field->getClean());
+            $errors = array_merge($errors, $field->errors());
+        }
+        if (count($errors) === 0)
+            $this->set('configuration', JsonDataEncoder::encode($config));
+        return count($errors) === 0;
+    }
+
+    function getImpl() {
+        if (!isset($this->_impl)) {
+            if (!($I = self::lookupByType($this->type, $this)))
+                throw new Exception(sprintf(
+                    '%s: No such filter action registered', $this->type));
+            $this->_impl = $I;
+        }
+        return $this->_impl;
+    }
+
+    function apply(&$ticket, array $info) {
+        return $this->getImpl()->apply($ticket, $info);
+    }
+
+    function save($refetch=false) {
+        if ($this->dirty)
+            $this->updated = SqlFunction::NOW();
+        return parent::save($refetch || $this->dirty);
+    }
+
+    static function register($class, $group=false) {
+        if (!$class::$type)
+            throw new Exception('Filter actions must specify ::$type');
+        elseif (!is_subclass_of($class, 'TriggerAction'))
+            throw new Exception('Filter actions must extend from TriggerAction');
+
+        self::$registry[$class::$type] = $class;
+        self::$registry_group[$group ?: ''][$class::$type] = $class;
+    }
+
+    static function lookupByType($type, $thisObj=false) {
+        if (!isset(self::$registry[$type]))
+            return null;
+
+        $class = self::$registry[$type];
+        return new $class($thisObj);
+    }
+
+    static function allRegistered($group=false) {
+        $types = array();
+        foreach (self::$registry_group as $group=>$actions) {
+            $G = $group ? __($group) : '';
+            foreach ($actions as $type=>$class) {
+                $types[$G][$type] = __($class::getName());
+            }
+        }
+        return $types;
+    }
+}
+
+abstract class TriggerAction {
+    static $type = false;
+    static $flags = 0;
+
+    const FLAG_MULTI_USE    = 0x0001;   // Action can be used multiple times
+
+    var $action;
+
+    function __construct($action=false) {
+        $this->action = $action;
+    }
+
+    function getConfiguration() {
+        if ($this->action)
+            return $this->action->getConfiguration();
+        return array();
+    }
+
+    function getConfigurationForm($source=false) {
+        if (!$this->_cform) {
+            $config = $this->getConfiguration();
+            $options = $this->getConfigurationOptions();
+            // Find a uid offset for this guy
+            $uid = 1000;
+            foreach (FilterAction::$registry as $type=>$class) {
+                $uid += 100;
+                if ($type == $this->getType())
+                    break;
+            }
+            // Ensure IDs are unique
+            foreach ($options as $f) {
+                $f->set('id', $uid++);
+            }
+            $this->_cform = new Form($options, $source);
+            if (!$source) {
+                foreach ($this->_cform->getFields() as $name=>$f) {
+                    if ($config && isset($config[$name]))
+                        $f->value = $config[$name];
+                    elseif ($f->get('default'))
+                        $f->value = $f->get('default');
+                }
+            }
+        }
+        return $this->_cform;
+    }
+
+    function hasFlag($flag) {
+        return static::$flags & $flag > 0;
+    }
+
+    static function getType() { return static::$type; }
+    static function getName() { return __(static::$name); }
+
+    abstract function apply(&$ticket, array $info);
+    abstract function getConfigurationOptions();
+}
+
+class FA_RejectTicket extends TriggerAction {
+    static $type = 'reject';
+    static $name = /* @trans */ 'Reject Ticket';
+
+    function apply(&$ticket, array $info) {
+        throw new RejectedException($this->action->getFilter(), $ticket);
+    }
+
+    function getConfigurationOptions() {
+        return array(
+            '' => new FreeTextField(array(
+                'configuration' => array(
+                    'content' => sprintf('<span style="color:red"><b>%s</b></span>',
+                        __('Reject Ticket')),
+                )
+            )),
+        );
+    }
+}
+FilterAction::register('FA_RejectTicket', /* @trans */ 'Ticket');
+
+class FA_UseReplyTo extends TriggerAction {
+    static $type = 'replyto';
+    static $name = /* @trans */ 'Use Reply-To Email';
+
+    function apply(&$ticket, array $info) {
+        $config = $this->getConfiguration();
+        if ($config['enable'] && $info['reply-to']) {
+            $ticket['email'] = $info['reply-to'];
+            if ($info['reply-to-name'])
+                $ticket['name'] = $info['reply-to-name'];
+        }
+    }
+
+    function getConfigurationOptions() {
+        return array(
+            '' => new FreeTextField(array(
+                'configuration' => array(
+                    'content' => __('<strong>Use</strong> the Reply-To email header')
+                )
+            )),
+        );
+    }
+}
+FilterAction::register('FA_UseReplyTo', /* @trans */ 'Communication');
+
+class FA_DisableAutoResponse extends TriggerAction {
+    static $type = 'noresp';
+    static $name = /* @trans */ "Disable autoresponse";
+
+    function apply(&$ticket, array $info) {
+        # TODO: Disable alerting
+        # XXX: Does this imply turning it on as well? (via ->sendAlerts())
+        $config = $this->getConfiguration();
+        if ($config['enable']) {
+            $ticket['autorespond']=false;
+        }
+    }
+
+    function getConfigurationOptions() {
+        return array(
+            '' => new FreeTextField(array(
+                'configuration' => array(
+                    'content' => __('<strong>Disable</strong> new ticket auto-response')
+                ),
+            )),
+        );
+    }
+}
+FilterAction::register('FA_DisableAutoResponse', /* @trans */ 'Communication');
+
+class FA_AutoCannedResponse extends TriggerAction {
+    static $type = 'canned';
+    static $name = /* @trans */ "Attach Canned Response";
+
+    function apply(&$ticket, array $info) {
+        $config = $this->getConfiguration();
+        if ($config['canned_id']) {
+            $ticket['cannedResponseId'] = $config['canned_id'];
+        }
+    }
+
+    function getConfigurationOptions() {
+        $sql='SELECT canned_id, title, isenabled FROM '.CANNED_TABLE .' ORDER by title';
+        $choices = array(false => '— '.__('None').' —');
+        if ($res=db_query($sql)) {
+            while (list($id, $title, $isenabled)=db_fetch_row($res)) {
+                if (!$isenabled)
+                    $title .= ' ' . __('(disabled)');
+                $choices[$id] = $title;
+            }
+        }
+        return array(
+            'canned_id' => new ChoiceField(array(
+                'default' => false,
+                'choices' => $choices,
+            )),
+        );
+    }
+}
+FilterAction::register('FA_AutoCannedResponse', /* @trans */ 'Communication');
+
+class FA_RouteDepartment extends TriggerAction {
+    static $type = 'dept';
+    static $name = /* @trans */ 'Set Department';
+
+    function apply(&$ticket, array $info) {
+        $config = $this->getConfiguration();
+        if ($config['dept_id'])
+            $ticket['deptId'] = $config['dept_id'];
+    }
+
+    function getConfigurationOptions() {
+        return array(
+            'dept_id' => new ChoiceField(array(
+                'configuration' => array('prompt' => __('Unchanged')),
+                'choices' => Dept::getDepartments(),
+            )),
+        );
+    }
+}
+FilterAction::register('FA_RouteDepartment', /* @trans */ 'Ticket');
+
+class FA_AssignPriority extends TriggerAction {
+    static $type = 'pri';
+    static $name = /* @trans */ "Set Priority";
+
+    function apply(&$ticket, array $info) {
+        $config = $this->getConfiguration();
+        if ($config['priority'])
+            $ticket['priority_id'] = $config['priority']->getId();
+    }
+
+    function getConfigurationOptions() {
+        $sql = 'SELECT priority_id, priority_desc FROM '.PRIORITY_TABLE
+              .' ORDER BY priority_urgency DESC';
+        $choices = array();
+        if ($res = db_query($sql)) {
+            while ($row = db_fetch_row($res))
+                $choices[$row[0]] = $row[1];
+        }
+        return array(
+            'priority' => new ChoiceField(array(
+                'configuration' => array('prompt' => __('Unchanged')),
+                'choices' => $choices,
+            )),
+        );
+    }
+}
+FilterAction::register('FA_AssignPriority', /* @trans */ 'Ticket');
+
+class FA_AssignSLA extends TriggerAction {
+    static $type = 'sla';
+    static $name = /* @trans */ 'Set SLA Plan';
+
+    function apply(&$ticket, array $info) {
+        $config = $this->getConfiguration();
+        if ($config['sla_id'])
+            $ticket['slaId'] = $config['sla_id'];
+    }
+
+    function getConfigurationOptions() {
+        $choices = SLA::getSLAs();
+        return array(
+            'sla_id' => new ChoiceField(array(
+                'configuration' => array('prompt' => __('Unchanged')),
+                'choices' => $choices,
+            )),
+        );
+    }
+}
+FilterAction::register('FA_AssignSLA', /* @trans */ 'Ticket');
+
+class FA_AssignTeam extends TriggerAction {
+    static $type = 'team';
+    static $name = /* @trans */ 'Assign Team';
+
+    function apply(&$ticket, array $info) {
+        $config = $this->getConfiguration();
+        if ($config['team_id'])
+            $ticket['teamId'] = $config['team_id'];
+    }
+
+    function getConfigurationOptions() {
+        $choices = Team::getTeams();
+        return array(
+            'team_id' => new ChoiceField(array(
+                'configuration' => array('prompt' => __('Unchanged')),
+                'choices' => $choices,
+            )),
+        );
+    }
+}
+FilterAction::register('FA_AssignTeam', /* @trans */ 'Ticket');
+
+class FA_AssignAgent extends TriggerAction {
+    static $type = 'agent';
+    static $name = /* @trans */ 'Assign Agent';
+
+    function apply(&$ticket, array $info) {
+        $config = $this->getConfiguration();
+        if ($config['staff_id'])
+            $ticket['staffId'] = $config['staff_id'];
+    }
+
+    function getConfigurationOptions() {
+        $choices = Staff::getStaffMembers();
+        return array(
+            'staff_id' => new ChoiceField(array(
+                'configuration' => array('prompt' => __('Unchanged')),
+                'choices' => $choices,
+            )),
+        );
+    }
+}
+FilterAction::register('FA_AssignAgent', /* @trans */ 'Ticket');
+
+class FA_AssignTopic extends TriggerAction {
+    static $type = 'topic';
+    static $name = /* @trans */ 'Set Help Topic';
+
+    function apply(&$ticket, array $info) {
+        $config = $this->getConfiguration();
+        if ($config['topic_id'])
+            $ticket['topicId'] = $config['topic_id'];
+    }
+
+    function getConfigurationOptions() {
+        $choices = HelpTopic::getAllHelpTopics();
+        return array(
+            'topic_id' => new ChoiceField(array(
+                'configuration' => array('prompt' => __('Unchanged')),
+                'choices' => $choices,
+            )),
+        );
+    }
+}
+FilterAction::register('FA_AssignTopic', /* @trans */ 'Ticket');
+
+class FA_SetStatus extends TriggerAction {
+    static $type = 'status';
+    static $name = /* @trans */ 'Set Ticket Status';
+
+    function apply(&$ticket, array $info) {
+        $config = $this->getConfiguration();
+        if ($config['status_id'])
+            $ticket['statusId'] = $config['status_id'];
+    }
+
+    function getConfigurationOptions() {
+        $choices = array();
+        foreach (TicketStatusList::getStatuses() as $S) {
+            // TODO: Move this to TicketStatus::getName
+            $name = $S->getName();
+            if (!($isenabled = $S->isEnabled()))
+                $name.=' '.__('(disabled)');
+            $choices[$S->getId()] = $name;
+        }
+        return array(
+            'status_id' => new ChoiceField(array(
+                'configuration' => array('prompt' => __('Unchanged')),
+                'choices' => $choices,
+            )),
+        );
+    }
+}
+FilterAction::register('FA_SetStatus', /* @trans */ 'Ticket');
+
+class FA_SendEmail extends TriggerAction {
+    static $type = 'email';
+    static $name = /* @trans */ 'Send an Email';
+    static $flags = TriggerAction::FLAG_MULTI_USE;
+
+    function apply(&$ticket, array $info) {
+        global $ost;
+
+        $config = $this->getConfiguration();
+        $info = array('subject' => $config['subject'],
+            'message' => $config['message']);
+        $info = $ost->replaceTemplateVariables(
+            $info, array('ticket' => $ticket)
+        );
+
+        // Honor FROM address settings
+        if (!$config['from'] || !($mailer = Email::lookup($config['from'])))
+            $mailer = new Mailer();
+
+        // Honor %{user} variable
+        $to = $config['recipients'];
+        $replacer = new VariableReplacer();
+        $replacer->assign(array(
+            'user' => sprintf('%s <%s>', $ticket['name'], $ticket['email'])
+        ));
+        $to = $replacer->replaceVars($to);
+
+        $mailer->send($to, $info['subject'], $info['message']);
+    }
+
+    function getConfigurationOptions() {
+        $choices = array('' => __('Default System Email'));
+        $choices += EmailModel::getAddresses();
+
+        return array(
+            'recipients' => new TextboxField(array(
+                'label' => __('Recipients'), 'required' => true,
+                'configuration' => array(
+                    'size' => 80, 'length' => 1000,
+                ),
+                'validators' => function($self, $value) {
+                    if (!($mails = Mail_RFC822::parseAddressList($value)) || PEAR::isError($mails))
+                        $self->addError('Unable to parse address list. '
+                            .'Use commas to separate addresses.');
+
+                    $valid = array('user',);
+                    foreach ($mails as $M) {
+                        // Check placeholders like '%{user}'
+                        $P = array();
+                        if (preg_match('`%\{([^}]+)\}`', $M->mailbox, $P)) {
+                            if (!in_array($P[1], $valid))
+                                $self->addError(sprintf('%s: Not a valid variable', $P[0]));
+                        }
+                        elseif ($M->host == 'localhost' || !$M->mailbox) {
+                            $self->addError(sprintf(__('%s: Not a valid email address'),
+                                $M->mailbox . '@' . $M->host));
+                        }
+                    }
+                }
+            )),
+            'subject' => new TextboxField(array(
+                'configuration' => array(
+                    'size' => 80,
+                    'placeholder' => __('Subject')
+                ),
+            )),
+            'message' => new TextareaField(array(
+                'configuration' => array(
+                    'placeholder' => __('Message'),
+                    'html' => true,
+                ),
+            )),
+            'from' => new ChoiceField(array(
+                'label' => __('From Email'),
+                'choices' => $choices,
+                'default' => '',
+            )),
+        );
+    }
+}
+FilterAction::register('FA_SendEmail', /* @trans */ 'Communication');
diff --git a/include/class.forms.php b/include/class.forms.php
index b101009c42239d20e358a75ec799fd67b50f312d..dfccf962c5d1e307fec038fd028f400c889de122 100644
--- a/include/class.forms.php
+++ b/include/class.forms.php
@@ -2759,11 +2759,18 @@ class FreeTextField extends FormField {
 class FreeTextWidget extends Widget {
     function render($options=array()) {
         $config = $this->field->getConfiguration();
-        ?><div class=""><h3><?php
-            echo Format::htmlchars($this->field->getLocal('label'));
-        ?></h3><em><?php
-            echo Format::htmlchars($this->field->getLocal('hint'));
-        ?></em><div><?php
+        ?><div class=""><?php
+        if ($label = $this->field->getLocal('label')) { ?>
+            <h3><?php
+            echo Format::htmlchars($label);
+        ?></h3><?php
+        }
+        if ($hint = $this->field->getLocal('hint')) { ?>
+        <em><?php
+            echo Format::htmlchars($hint);
+        ?></em><?php
+        } ?>
+        <div><?php
             echo Format::viewableImages($config['content']); ?></div>
         </div>
         <?php
diff --git a/include/class.ticket.php b/include/class.ticket.php
index 1dd62e97cf49c0be0a0a727260f54062f8d0c8b3..07c8bfebdf10e7a520974ab0732204a9040f7701 100644
--- a/include/class.ticket.php
+++ b/include/class.ticket.php
@@ -526,6 +526,23 @@ class Ticket {
         return $this->ht['status_id'];
     }
 
+    /**
+     * setStatusId
+     *
+     * Forceably set the ticket status ID to the received status ID. No
+     * checks are made. Use ::setStatus() to change the ticket status
+     */
+    // XXX: Use ::setStatus to change the status. This can be used as a
+    //      fallback if the logic in ::setStatus fails.
+    function setStatusId($id) {
+        $sql = 'UPDATE '.TICKET_TABLE.' SET updated=NOW() '.
+               ' ,status_id='.db_input($status->getId()) .
+               ' WHERE ticket_id='.db_input($this->getId());
+
+        if (!db_query($sql) || !db_affected_rows())
+            return false;
+    }
+
     function getStatus() {
 
         if (!$this->status && $this->getStatusId())
@@ -1076,7 +1093,7 @@ class Ticket {
     function setStatus($status, $comments='', &$errors=array(), $set_closing_agent=true) {
         global $thisstaff;
 
-        if (!$thisstaff || !($role=$thisstaff->getRole($this->getDeptId())))
+        if ($thisstaff && !($role=$thisstaff->getRole($this->getDeptId())))
             return false;
 
         if ($status && is_numeric($status))
@@ -1085,19 +1102,21 @@ class Ticket {
         if (!$status || !$status instanceof TicketStatus)
             return false;
 
-        // Double check permissions
-        switch ($status->getState()) {
-        case 'closed':
-            if (!($role->hasPerm(TicketModel::PERM_CLOSE)))
+        // Double check permissions (when changing status)
+        if ($role && $this->getStatusId()) {
+            switch ($status->getState()) {
+            case 'closed':
+                if (!($role->hasPerm(TicketModel::PERM_CLOSE)))
+                    return false;
+                break;
+            case 'deleted':
+                // XXX: intercept deleted status and do hard delete
+                if ($role->hasPerm(TicketModel::PERM_DELETE))
+                    return $this->delete($comments);
+                // Agent doesn't have permission to delete  tickets
                 return false;
-            break;
-        case 'deleted':
-            // XXX: intercept deleted status and do hard delete
-            if ($role->hasPerm(TicketModel::PERM_DELETE))
-                return $this->delete($comments);
-            // Agent doesn't have permission to delete  tickets
-            return false;
-            break;
+                break;
+            }
         }
 
         if ($this->getStatusId() == $status->getId())
@@ -1244,10 +1263,14 @@ class Ticket {
                 return false;  //bail out...missing stuff.
         }
 
-        $options = array(
-            'inreplyto'=>$message->getEmailMessageId(),
-            'references'=>$message->getEmailReferences(),
-            'thread'=>$message);
+        $options = array();
+        if ($message instanceof ThreadEntry) {
+            $options += array(
+                'inreplyto'=>$message->getEmailMessageId(),
+                'references'=>$message->getEmailReferences(),
+                'thread'=>$message
+            );
+        }
 
         //Send auto response - if enabled.
         if($autorespond
@@ -1275,7 +1298,7 @@ class Ticket {
 
             $recipients=$sentlist=array();
             //Exclude the auto responding email just incase it's from staff member.
-            if ($message->isAutoReply())
+            if ($message instanceof ThreadEntry && $message->isAutoReply())
                 $sentlist[] = $this->getEmail();
 
             //Alert admin??
@@ -2019,7 +2042,7 @@ class Ticket {
         return $message;
     }
 
-    function postCannedReply($canned, $msgId, $alert=true) {
+    function postCannedReply($canned, $message, $alert=true) {
         global $ost, $cfg;
 
         if((!is_object($canned) && !($canned=Canned::lookup($canned))) || !$canned->isEnabled())
@@ -2036,7 +2059,7 @@ class Ticket {
             $response = new TextThreadEntryBody(
                     $this->replaceVars($canned->getPlainText()));
 
-        $info = array('msgId' => $msgId,
+        $info = array('msgId' => $message instanceof ThreadEntry ? $message->getId() : 0,
                       'poster' => __('SYSTEM (Canned Reply)'),
                       'response' => $response,
                       'cannedattachments' => $files);
@@ -2715,15 +2738,11 @@ class Ticket {
             }
         }
 
-        if (!$form->isValid($field_filter('ticket')))
-            $errors += $form->errors();
-
         if ($vars['uid'])
             $user = User::lookup($vars['uid']);
 
         $id=0;
         $fields=array();
-        $fields['message']  = array('type'=>'*',     'required'=>1, 'error'=>__('Message content is required'));
         switch (strtolower($origin)) {
             case 'web':
                 $fields['topicId']  = array('type'=>'int',  'required'=>1, 'error'=>__('Select a help topic'));
@@ -2756,22 +2775,45 @@ class Ticket {
                 $errors['duedate']=__('Due date must be in the future');
         }
 
+        $topic_forms = array();
         if (!$errors) {
 
-            # Perform ticket filter actions on the new ticket arguments
-            $__form = null;
+            // Handle the forms associate with the help topics. Instanciate the
+            // entries, disable and track the requested disabled fields.
             if ($vars['topicId']) {
-                if (($__topic=Topic::lookup($vars['topicId']))
-                    && ($__form = $__topic->getForm())
-                ) {
-                    $__form = $__form->instanciate();
-                    $__form->setSource($vars);
+                if ($__topic=Topic::lookup($vars['topicId'])) {
+                    foreach ($__topic->getForms() as $idx=>$__F) {
+                        $disabled = array();
+                        foreach ($__F->getFields() as $field) {
+                            if (!$field->isEnabled() && $field->hasFlag(DynamicFormField::FLAG_ENABLED))
+                                $disabled[] = $field->get('id');
+                        }
+                        // Special handling for the ticket form — disable fields
+                        // requested to be disabled as per the help topic.
+                        if ($__F->get('type') == 'T') {
+                            foreach ($form->getFields() as $field) {
+                                if (false !== array_search($field->get('id'), $disabled))
+                                    $field->disable();
+                            }
+                            $form->sort = $idx;
+                            $__F = $form;
+                        }
+                        else {
+                            $__F = $__F->instanciate($idx);
+                            $__F->setSource($vars);
+                            $topic_forms[] = $__F;
+                        }
+                        // Track fields currently disabled
+                        $__F->extra = JsonDataEncoder::encode(array(
+                            'disable' => $disabled
+                        ));
+                    }
                 }
             }
 
             try {
                 $vars = self::filterTicketData($origin, $vars,
-                    array($form, $__form), $user);
+                    array_merge(array($form), $topic_forms), $user);
             }
             catch (RejectedException $ex) {
                 return $reject_ticket(
@@ -2823,12 +2865,13 @@ class Ticket {
             }
         }
 
+        if (!$form->isValid($field_filter('ticket')))
+            $errors += $form->errors();
+
         if ($vars['topicId']) {
             if ($topic=Topic::lookup($vars['topicId'])) {
-                if ($topic_form = $topic->getForm()) {
-                    $TF = $topic_form->getForm($vars);
-                    $topic_form = $topic_form->instanciate();
-                    $topic_form->setSource($vars);
+                foreach ($topic_forms as $topic_form) {
+                    $TF = $topic_form->getForm()->getForm($vars);
                     if (!$TF->isValid($field_filter('topic')))
                         $errors = array_merge($errors, $TF->errors());
                 }
@@ -2972,7 +3015,7 @@ class Ticket {
         $form->save();
 
         // Save the form data from the help-topic form, if any
-        if ($topic_form) {
+        foreach ($topic_forms as $topic_form) {
             $topic_form->setTicketId($id);
             $topic_form->save();
         }
@@ -3032,8 +3075,12 @@ class Ticket {
         // Apply requested status — this should be done AFTER assignment,
         // because if it is requested to be closed, it should not cause the
         // ticket to be reopened for assignment.
-        if ($statusId)
-            $ticket->setStatus($statusId, false, $errors, false);
+        if ($statusId) {
+            if (!$ticket->setStatus($statusId, false, $errors, false)) {
+                // Tickets _must_ have a status. Forceably set one here
+                $ticket->setStatusId($cfg->getDefaultTicketStatusId());
+            }
+        }
 
         /**********   double check auto-response  ************/
         //Override auto responder if the FROM email is one of the internal emails...loop control.
@@ -3044,12 +3091,12 @@ class Ticket {
         # not have a return 'ping' message
         if (isset($vars['flags']) && $vars['flags']['bounce'])
             $autorespond = false;
-        if ($autorespond && $message->isAutoReply())
+        if ($autorespond && $message instanceof ThreadEntry && $message->isAutoReply())
             $autorespond = false;
 
         //post canned auto-response IF any (disables new ticket auto-response).
         if ($vars['cannedResponseId']
-            && $ticket->postCannedReply($vars['cannedResponseId'], $message->getId(), $autorespond)) {
+            && $ticket->postCannedReply($vars['cannedResponseId'], $message, $autorespond)) {
                 $ticket->markUnAnswered(); //Leave the ticket as unanswred.
                 $autorespond = false;
         }
@@ -3061,7 +3108,7 @@ class Ticket {
 
         //Don't send alerts to staff when the message is a bounce
         //  this is necessary to avoid possible loop (especially on new ticket)
-        if ($alertstaff && $message->isBounce())
+        if ($alertstaff && $message instanceof ThreadEntry && $message->isBounce())
             $alertstaff = false;
 
         /***** See if we need to send some alerts ****/
diff --git a/include/class.topic.php b/include/class.topic.php
index 5adc575635e0fc40f214778ad4da2d2622948177..eda6a2a69b3c9e59af82745855cf30ef5d62c8d4 100644
--- a/include/class.topic.php
+++ b/include/class.topic.php
@@ -52,11 +52,15 @@ class Topic extends VerySimpleModel {
                     'priority_id' => 'Priority.priority_id',
                 ),
             ),
+            'forms' => array(
+                'reverse' => 'TopicFormModel.topic',
+                'null' => true,
+            ),
         ),
     );
 
     var $page;
-    var $form;
+    var $_forms;
 
     const DISPLAY_DISABLED = 2;
 
@@ -129,19 +133,15 @@ class Topic extends VerySimpleModel {
         return $this->page;
     }
 
-    function getFormId() {
-        return $this->form_id;
-    }
-
-    function getForm() {
-        $id = $this->getFormId();
-
-        if ($id == self::FORM_USE_PARENT && ($p = $this->getParent()))
-            $this->form = $p->getForm();
-        elseif ($id && !$this->form)
-            $this->form = DynamicForm::lookup($id);
-
-        return $this->form;
+    function getForms() {
+        if (!isset($this->_forms)) {
+            foreach ($this->forms->select_related('form') as $F) {
+                $extra = JsonDataParser::decode($F->extra) ?: array();
+                $F->form->disableFields($extra['disable'] ?: array());
+                $this->_forms[] = $F->form;
+            }
+        }
+        return $this->_forms;
     }
 
     function autoRespond() {
@@ -426,12 +426,69 @@ class Topic extends VerySimpleModel {
             $errors['err']=sprintf(__('Unable to update %s.'), __('this help topic'))
             .' '.__('Internal error occurred');
         }
-        if (!$cfg || $cfg->getTopicSortMode() == 'a') {
-            static::updateSortOrder();
+        if ($rv) {
+            if (!$cfg || $cfg->getTopicSortMode() == 'a') {
+                static::updateSortOrder();
+            }
+            $this->updateForms($vars, $errors);
         }
         return $rv;
     }
 
+    function updateForms($vars, &$errors) {
+        $find_disabled = function($form) use ($vars) {
+            $fields = $vars['fields'];
+            $disabled = array();
+            foreach ($form->fields->values_flat('id') as $row) {
+                list($id) = $row;
+                if (false === ($idx = array_search($id, $fields))) {
+                    $disabled[] = $id;
+                }
+            }
+            return $disabled;
+        };
+
+        // Consider all the forms in the request
+        $current = array();
+        if (is_array($form_ids = $vars['forms'])) {
+            $forms = TopicFormModel::objects()
+                ->select_related('form')
+                ->filter(array('topic_id' => $this->getId()));
+            foreach ($forms as $F) {
+                if (false !== ($idx = array_search($F->form_id, $form_ids))) {
+                    $current[] = $F->form_id;
+                    $F->sort = $idx + 1;
+                    $F->extra = JsonDataEncoder::encode(
+                        array('disable' => $find_disabled($F->form))
+                    );
+                    $F->save();
+                    unset($form_ids[$idx]);
+                }
+                elseif ($F->form->get('type') != 'T') {
+                    $F->delete();
+                }
+            }
+            foreach ($form_ids as $sort=>$id) {
+                if (!($form = DynamicForm::lookup($id))) {
+                    continue;
+                }
+                elseif (in_array($id, $current)) {
+                    // Don't add a form more than once
+                    continue;
+                }
+                TopicFormModel::create(array(
+                    'topic_id' => $this->getId(),
+                    'form_id' => $id,
+                    'sort' => $sort + 1,
+                    'extra' => JsonDataEncoder::encode(
+                        array('disable' => $find_disabled($form))
+                    )
+                ))->save();
+            }
+        }
+        return true;
+    }
+
     function save($refetch=false) {
         if ($this->dirty)
             $this->updated = SqlFunction::NOW();
@@ -473,3 +530,19 @@ class Topic extends VerySimpleModel {
 
 // Add fields from the standard ticket form to the ticket filterable fields
 Filter::addSupportedMatches(/* @trans */ 'Help Topic', array('topicId' => 'Topic ID'), 100);
+
+class TopicFormModel extends VerySimpleModel {
+    static $meta = array(
+        'table' => TOPIC_FORM_TABLE,
+        'pk' => array('id'),
+        'ordering' => array('sort'),
+        'joins' => array(
+            'topic' => array(
+                'constraint' => array('topic_id' => 'Topic.topic_id'),
+            ),
+            'form' => array(
+                'constraint' => array('form_id' => 'DynamicForm.id'),
+            ),
+        ),
+    );
+}
diff --git a/include/class.variable.php b/include/class.variable.php
index 41bcb619e5587ddce96dce8bdb16d1e5aec7994b..ffb850ec8f264dd2e15ada81e37e90b268c0ccec 100644
--- a/include/class.variable.php
+++ b/include/class.variable.php
@@ -79,6 +79,9 @@ class VariableReplacer {
             return $this->getVar($rv, $part);
         }
 
+        if (is_array($obj) && isset($obj[$v]))
+            return $obj[$v];
+
         if (!$var || !method_exists($obj, 'getVar'))
             return "";
 
@@ -112,8 +115,13 @@ class VariableReplacer {
         $parts = explode('.', $var, 2);
         if($parts && ($obj=$this->getObj($parts[0])))
             return $this->getVar($obj, $parts[1]);
-        elseif($parts[0] && @isset($this->variables[$parts[0]])) //root override
+        elseif($parts[0] && @isset($this->variables[$parts[0]])) { //root override
+            if (is_array($this->variables[$parts[0]])
+                    && isset($this->variables[$parts[0]][$parts[1]]))
+                return $this->variables[$parts[0]][$parts[1]];
+
             return $this->variables[$parts[0]];
+        }
 
         //Unknown object or variable - leavig it alone.
         $this->setError(sprintf(__('Unknown object for "%s" tag'), $var));
diff --git a/include/client/open.inc.php b/include/client/open.inc.php
index 821aa6ef9cc8661e5126b070dc5d35fedc09d1df..e29f2b651be929de68e8d50f759e7bef41c28c33 100644
--- a/include/client/open.inc.php
+++ b/include/client/open.inc.php
@@ -13,11 +13,14 @@ $form = null;
 if (!$info['topicId'])
     $info['topicId'] = $cfg->getDefaultTopicId();
 
+$forms = array();
 if ($info['topicId'] && ($topic=Topic::lookup($info['topicId']))) {
-    $form = $topic->getForm();
-    if ($_POST && $form) {
-        $form = $form->instanciate();
-        $form->isValidForClient();
+    foreach ($topic->getForms() as $F) {
+        if ($_POST) {
+            $F = $F->instanciate();
+            $F->isValidForClient();
+        }
+        $forms[] = $F;
     }
 }
 
@@ -29,9 +32,26 @@ if ($info['topicId'] && ($topic=Topic::lookup($info['topicId']))) {
   <input type="hidden" name="a" value="open">
   <table width="800" cellpadding="1" cellspacing="0" border="0">
     <tbody>
+<?php
+        if (!$thisclient) {
+            $uform = UserForm::getUserForm()->getForm($_POST);
+            if ($_POST) $uform->isValid();
+            $uform->render(false);
+        }
+        else { ?>
+            <tr><td colspan="2"><hr /></td></tr>
+        <tr><td><?php echo __('Email'); ?>:</td><td><?php echo $thisclient->getEmail(); ?></td></tr>
+        <tr><td><?php echo __('Client'); ?>:</td><td><?php echo $thisclient->getName(); ?></td></tr>
+        <?php } ?>
+    </tbody>
+    <tbody>
+    <tr><td colspan="2"><hr />
+        <div class="form-header" style="margin-bottom:0.5em">
+        <b><?php echo __('Help Topic'); ?></b>
+        </div>
+    </td></tr>
     <tr>
-        <td class="required"><?php echo __('Help Topic');?>:</td>
-        <td>
+        <td colspan="2">
             <select id="topicId" name="topicId" onchange="javascript:
                     var data = $(':input[name]', '#dynamic-form').serialize();
                     $.ajax(
@@ -59,28 +79,21 @@ if ($info['topicId'] && ($topic=Topic::lookup($info['topicId']))) {
             <font class="error">*&nbsp;<?php echo $errors['topicId']; ?></font>
         </td>
     </tr>
-<?php
-        if (!$thisclient) {
-            $uform = UserForm::getUserForm()->getForm($_POST);
-            if ($_POST) $uform->isValid();
-            $uform->render(false);
-        }
-        else { ?>
-            <tr><td colspan="2"><hr /></td></tr>
-        <tr><td><?php echo __('Email'); ?>:</td><td><?php echo $thisclient->getEmail(); ?></td></tr>
-        <tr><td><?php echo __('Client'); ?>:</td><td><?php echo $thisclient->getName(); ?></td></tr>
-        <?php } ?>
     </tbody>
     <tbody id="dynamic-form">
-        <?php if ($form) {
+        <?php foreach ($forms as $form) {
+            $hasFields = false;
+            foreach ($form->getFields() as $f) {
+                if ($f->isVisibleToUsers()) {
+                    $hasFields = true;
+                    break;
+                }
+            }
+            if (!$hasFields)
+                continue;
             include(CLIENTINC_DIR . 'templates/dynamic-form.tmpl.php');
         } ?>
     </tbody>
-    <tbody><?php
-        $tform = TicketForm::getInstance()->getForm($_POST);
-        if ($_POST) $tform->isValid();
-        $tform->render(false); ?>
-    </tbody>
     <tbody>
     <?php
     if($cfg && $cfg->isCaptchaEnabled() && (!$thisclient || !$thisclient->isValid())) {
diff --git a/include/client/templates/dynamic-form.tmpl.php b/include/client/templates/dynamic-form.tmpl.php
index a333ac2eef0d7aff31ee0a35d7cc18f4717c3bc5..6cf54466b08a0622693126a915ed503cf9dbba86 100644
--- a/include/client/templates/dynamic-form.tmpl.php
+++ b/include/client/templates/dynamic-form.tmpl.php
@@ -8,7 +8,7 @@
     <?php print ($form instanceof DynamicFormEntry)
         ? $form->getForm()->getMedia() : $form->getMedia(); ?>
     <h3><?php echo Format::htmlchars($form->getTitle()); ?></h3>
-    <em><?php echo Format::htmlchars($form->getInstructions()); ?></em>
+    <div><?php echo Format::display($form->getInstructions()); ?></div>
     </div>
     </td></tr>
     <?php
diff --git a/include/staff/dynamic-form.inc.php b/include/staff/dynamic-form.inc.php
index 2d6e64f496f86f99f3743d34869ed8b328c95723..1939cda0e211de50eaddf249e51243853750ad7a 100644
--- a/include/staff/dynamic-form.inc.php
+++ b/include/staff/dynamic-form.inc.php
@@ -7,9 +7,20 @@ if($form && $_REQUEST['a']!='add') {
     $url = "?id=".urlencode($_REQUEST['id']);
     $submit_text=__('Save Changes');
     $info = $form->ht;
-    $trans['title'] = $form->getTranslateTag('title');
-    $trans['instructions'] = $form->getTranslateTag('instructions');
+    $trans = array(
+        'title' => $form->getTranslateTag('title'),
+        'instructions' => $form->getTranslateTag('instructions'),
+    );
     $newcount=2;
+    $translations = CustomDataTranslation::allTranslations($trans, 'phrase');
+    $_keys = array_flip($trans);
+    foreach ($translations as $t) {
+        if (!Internationalization::isLanguageInstalled($t->lang))
+            continue;
+        // Create keys of [trans][de_DE][title] for instance
+        $info['trans'][$t->lang][$_keys[$t->object_hash]]
+            = Format::viewableImages($t->text);
+    }
 } else {
     $title = __('Add new custom form section');
     $action = 'add';
@@ -39,23 +50,65 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
     </thead>
     <tbody style="vertical-align:top">
         <tr>
-            <td width="180" class="required"><?php echo __('Title'); ?>:</td>
-            <td><input type="text" name="title" size="40"
-                data-translate-tag="<?php echo $trans['title']; ?>"
+            <td colspan="2">
+    <table class="full-width"><tbody><tr><td style="vertical-align:top">
+<?php
+$langs = Internationalization::getConfiguredSystemLanguages();
+if ($form && count($langs) > 1) { ?>
+    <ul class="vertical tabs" id="translations">
+        <li class="empty"><i class="icon-globe" title="This content is translatable"></i></li>
+<?php foreach ($langs as $tag=>$nfo) { ?>
+    <li class="<?php if ($tag == $cfg->getPrimaryLanguage()) echo "active";
+        ?>"><a href="#translation-<?php echo $tag; ?>" title="<?php
+        echo Internationalization::getLanguageDescription($tag);
+    ?>"><span class="flag flag-<?php echo strtolower($nfo['flag']); ?>"></span>
+    </a></li>
+<?php } ?>
+    </ul>
+<?php
+} ?>
+    </td>
+    <td id="translations_container">
+        <div id="translation-<?php echo $cfg->getPrimaryLanguage(); ?>" class="tab_content"
+            lang="<?php echo $cfg->getPrimaryLanguage(); ?>">
+            <div class="required"><?php echo __('Title'); ?>:</div>
+            <div>
+            <input type="text" name="title" size="60"
                 value="<?php echo $info['title']; ?>"/>
                 <i class="help-tip icon-question-sign" href="#form_title"></i>
-                <font class="error"><?php
-                    if ($errors['title']) echo '<br/>'; echo $errors['title']; ?></font>
-            </td>
-        </tr>
-        <tr>
-            <td width="180"><?php echo __('Instructions'); ?>:</td>
-            <td><textarea name="instructions" rows="3" cols="40"
-                data-translate-tag="<?php echo $trans['instructions']; ?>"><?php
-                echo $info['instructions']; ?></textarea>
+                <div class="error"><?php
+                    if ($errors['title']) echo '<br/>'; echo $errors['title']; ?></div>
+            </div>
+            <div style="margin-top: 8px"><?php echo __('Instructions'); ?>:
                 <i class="help-tip icon-question-sign" href="#form_instructions"></i>
-            </td>
-        </tr>
+                </div>
+            <textarea name="instructions" rows="3" cols="40" class="richtext"><?php
+                echo $info['instructions']; ?></textarea>
+        </div>
+
+<?php if ($langs && $form) {
+    foreach ($langs as $tag=>$nfo) {
+        if ($tag == $cfg->getPrimaryLanguage())
+            continue; ?>
+        <div id="translation-<?php echo $tag; ?>" class="tab_content"
+            style="display:none;" lang="<?php echo $tag; ?>">
+        <div>
+            <div class="required"><?php echo __('Title'); ?>:</div>
+            <input type="text" name="trans[<?php echo $tag; ?>][title]" size="60"
+                value="<?php echo $info['trans'][$tag]['title']; ?>"/>
+                <i class="help-tip icon-question-sign" href="#form_title"></i>
+        </div>
+        <div style="margin-top: 8px"><?php echo __('Instructions'); ?>:
+            <i class="help-tip icon-question-sign" href="#form_instructions"></i>
+            </div>
+        <textarea name="trans[<?php echo $tag; ?>][instructions]" cols="21" rows="12"
+            style="width:100%" class="richtext"><?php
+            echo $info['trans'][$tag]['instructions']; ?></textarea>
+        </div>
+<?php }
+} ?>
+    </td></tr></tbody></table>
+        </td></tr>
     </tbody>
     </table>
     <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2">
diff --git a/include/staff/filter.inc.php b/include/staff/filter.inc.php
index f09731af64a0cc7f5455b3677b408bb208af29aa..61e563bc077c5ec1a4eeef94191e6408ab981b67 100644
--- a/include/staff/filter.inc.php
+++ b/include/staff/filter.inc.php
@@ -167,209 +167,87 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
         } ?>
         <tr>
             <th colspan="2">
-                <em><strong><?php echo __('Filter Actions');?></strong>: <?php
-                echo __('Can be overwridden by other filters depending on processing order.');?>&nbsp;</em>
+                <em><strong><?php echo __('Filter Actions');?></strong>:
+                <div><?php
+                    echo __('Can be overwridden by other filters depending on processing order.');
+                ?><br/><?php
+                    echo __('Actions are executed in the order declared below');
+                ?></em>
             </th>
         </tr>
-        <tr>
-            <td width="180">
-                <?php echo __('Reject Ticket');?>:
-            </td>
-            <td>
-                <input type="checkbox" name="reject_ticket" value="1" <?php echo $info['reject_ticket']?'checked="checked"':''; ?> >
-                    <strong><font class="error"><?php echo __('Reject Ticket');?></font></strong>
-                    &nbsp;<i class="help-tip icon-question-sign" href="#reject_ticket"></i>
-            </td>
-        </tr>
-        <tr>
-            <td width="180">
-                <?php echo __('Reply-To Email');?>:
-            </td>
-            <td>
-                <input type="checkbox" name="use_replyto_email" value="1" <?php echo $info['use_replyto_email']?'checked="checked"':''; ?> >
-                    <?php echo __('<strong>Use</strong> Reply-To Email');?> <em>(<?php echo __('if available');?>)</em>
-                    &nbsp;<i class="help-tip icon-question-sign" href="#reply_to_email"></i></em>
-            </td>
-        </tr>
-        <tr>
-            <td width="180">
-                <?php echo __('Ticket auto-response');?>:
-            </td>
-            <td>
-                <input type="checkbox" name="disable_autoresponder" value="1" <?php echo $info['disable_autoresponder']?'checked="checked"':''; ?> >
-                    <?php echo __('<strong>Disable</strong> auto-response.');?>
-                    &nbsp;<i class="help-tip icon-question-sign" href="#ticket_auto_response"></i>
-            </td>
-        </tr>
-        <tr>
-            <td width="180">
-                <?php echo __('Canned Response');?>:
-            </td>
-                <td>
-                <select name="canned_response_id">
-                    <option value="">&mdash; <?php echo __('None');?> &mdash;</option>
-                    <?php
-                    $sql='SELECT canned_id, title, isenabled FROM '.CANNED_TABLE .' ORDER by title';
-                    if ($res=db_query($sql)) {
-                        while (list($id, $title, $isenabled)=db_fetch_row($res)) {
-                            $selected=($info['canned_response_id'] &&
-                                    $id==$info['canned_response_id'])
-                                ? 'selected="selected"' : '';
-
-                            if (!$isenabled)
-                                $title .= ' ' . __('(disabled)');
-
-                            echo sprintf('<option value="%d" %s>%s</option>',
-                                $id, $selected, $title);
-                        }
-                    }
-                    ?>
-                </select>
-                &nbsp;<i class="help-tip icon-question-sign" href="#canned_response"></i>
-            </td>
-        </tr>
-        <tr>
-            <td width="180">
-                <?php echo __('Department');?>:
-            </td>
-            <td>
-                <select name="dept_id">
-                    <option value="">&mdash; <?php echo __('Default');?> &mdash;</option>
-                    <?php
-                    foreach (Dept::getDepartments() as $id=>$name) {
-                        $selected=($info['dept_id'] && $id==$info['dept_id'])?'selected="selected"':'';
-                        echo sprintf('<option value="%d" %s>%s</option>',$id,$selected,$name);
-                    }
-                    ?>
-                </select>
-                &nbsp;<span class="error">*&nbsp;<?php echo $errors['dept_id']; ?></span>&nbsp;<i class="help-tip icon-question-sign" href="#department"></i>
-            </td>
-        </tr>
-        <tr>
-            <td width="180">
-                <?php echo __('Status'); ?>:
-            </td>
-            <td>
-                <span>
-                <select name="status_id">
-                    <option value="">&mdash; <?php echo __('Default'); ?> &mdash;</option>
-                    <?php
-                    foreach (TicketStatusList::getStatuses() as $status) {
-                        $name = $status->getName();
-                        if (!($isenabled = $status->isEnabled()))
-                            $name.=' '.__('(disabled)');
-
-                        echo sprintf('<option value="%d" %s %s>%s</option>',
-                                $status->getId(),
-                                ($info['status_id'] == $status->getId())
-                                 ? 'selected="selected"' : '',
-                                 $isenabled ? '' : 'disabled="disabled"',
-                                 $name
-                                );
-                    }
-                    ?>
-                </select>
-                &nbsp;
-                <span class="error"><?php echo $errors['status_id']; ?></span>
-                <i class="help-tip icon-question-sign" href="#status"></i>
-                </span>
-            </td>
-        </tr>
-        <tr>
-            <td width="180">
-                <?php echo __('Priority');?>:
-            </td>
-            <td>
-                <select name="priority_id">
-                    <option value="">&mdash; <?php echo __('Default');?> &mdash;</option>
-                    <?php
-                    $sql='SELECT priority_id,priority_desc FROM '.PRIORITY_TABLE.' pri ORDER by priority_urgency DESC';
-                    if(($res=db_query($sql)) && db_num_rows($res)){
-                        while(list($id,$name)=db_fetch_row($res)){
-                            $selected=($info['priority_id'] && $id==$info['priority_id'])?'selected="selected"':'';
-                            echo sprintf('<option value="%d" %s>%s</option>',$id,$selected,$name);
-                        }
-                    }
-                    ?>
-                </select>
-                &nbsp;<span class="error">*&nbsp;<?php echo $errors['priority_id']; ?></span>
-                &nbsp;<i class="help-tip icon-question-sign" href="#priority"></i>
-            </td>
-        </tr>
-        <tr>
-            <td width="180">
-                <?php echo __('SLA Plan');?>:
-            </td>
-            <td>
-                <select name="sla_id">
-                    <option value="0">&mdash; <?php echo __('System Default');?> &mdash;</option>
-                    <?php
-                    if($slas=SLA::getSLAs()) {
-                        foreach($slas as $id =>$name) {
-                            echo sprintf('<option value="%d" %s>%s</option>',
-                                    $id, ($info['sla_id']==$id)?'selected="selected"':'',$name);
-                        }
-                    }
-                    ?>
-                </select>
-                &nbsp;<span class="error">&nbsp;<?php echo $errors['sla_id']; ?></span>
-                &nbsp;<i class="help-tip icon-question-sign" href="#sla_plan"></i>
-            </td>
-        </tr>
-        <tr>
-            <td width="180">
-                <?php echo __('Auto-assign To');?>:
-            </td>
-            <td>
-                <select name="assign">
-                    <option value="0">&mdash; <?php echo __('Unassigned');?> &mdash;</option>
-                    <?php
-                    if (($users=Staff::getStaffMembers())) {
-                        echo '<OPTGROUP label="'.__('Agents').'">';
-                        foreach($users as $id => $name) {
-                            $name = new PersonsName($name);
-                            $k="s$id";
-                            $selected = ($info['assign']==$k || $info['staff_id']==$id)?'selected="selected"':'';
-                            ?>
-                            <option value="<?php echo $k; ?>"<?php echo $selected; ?>><?php echo $name; ?></option>
-                        <?php
-                        }
-                        echo '</OPTGROUP>';
-                    }
-                    $sql='SELECT team_id, isenabled, name FROM '.TEAM_TABLE .' ORDER BY name';
-                    if ($teams = Team::getTeams()) {
-                        echo '<OPTGROUP label="'.__('Teams').'">';
-                        foreach ($teams as $id=>$name) {
-                            $k="t$id";
-                            $selected = ($info['assign']==$k || $info['team_id']==$id)?'selected="selected"':'';
-                            ?>
-                            <option value="<?php echo $k; ?>"<?php echo $selected; ?>><?php echo $name; ?></option>
-                        <?php
-                        }
-                        echo '</OPTGROUP>';
-                    }
-                    ?>
-                </select>
-                &nbsp;<span class="error">&nbsp;<?php echo
-                $errors['assign']; ?></span><i class="help-tip icon-question-sign" href="#auto_assign"></i>
+    </tbody>
+    <tbody id="dynamic-actions" class="sortable-rows">
+<?php
+$existing = array();
+if ($filter) { foreach ($filter->getActions() as $A) {
+    $existing[] = $A->type;
+?>
+        <tr style="background-color:white"><td><i class="icon-bolt icon-large icon-muted"></i>
+            <?php echo $A->getImpl()->getName(); ?>:</td>
+            <td><div style="position:relative"><?php
+                $form = $A->getImpl()->getConfigurationForm($_POST ?: false);
+                // XXX: Drop this when the ORM supports proper caching
+                $form->isValid();
+                include STAFFINC_DIR . 'templates/dynamic-form-simple.tmpl.php';
+?>
+                <input type="hidden" name="actions[]" value="I<?php echo $A->getId(); ?>"/>
+                <div class="pull-right" style="position:absolute;top:2px;right:2px;">
+                    <a href="#" title="<?php echo __('clear'); ?>" onclick="javascript:
+        if (!confirm(__('You sure?')))
+            return false;
+        $(this).closest('td').find('input[name=\'actions[]\']')
+            .val(function(i,v) { return 'D' + v.substring(1); });
+        $(this).closest('tr').fadeOut(400, function() { $(this).hide(); });
+        return false;"><i class="icon-trash"></i></a>
+                </div>
+</div>
             </td>
         </tr>
+<?php } } ?>
+    </tbody>
+    <tbody>
         <tr>
-            <td width="180">
-                <?php echo __('Help Topic'); ?>
-            </td>
+            <td><strong><i class="icon-plus-sign"></i>
+                <?php echo __('Add'); ?>:
+            </strong></td>
             <td>
-                <select name="topic_id">
-                    <option value="0" selected="selected">&mdash; <?php
-                        echo __('Unchanged'); ?> &mdash;</option>
-                    <?php
-                    foreach (Topic::getAllHelpTopics(true) as $id=>$name) {
-                        $selected=($info['topic_id'] && $id==$info['topic_id'])?'selected="selected"':'';
-                        echo sprintf('<option value="%d" %s>%s</option>',$id,$selected,$name);
-                    }
-                    ?>
+                <select name="new-action" id="new-action-select"
+                    onchange="javascript: $('#new-action-btn').trigger('click');">
+                    <option value=""><?php echo __('— Select an Action —'); ?></option>
+<?php
+$current_group = '';
+foreach (FilterAction::allRegistered() as $group=>$actions) {
+    if ($group && $current_group != $group) {
+        if ($current_group) echo '</optgroup>';
+        $current_group = $group;
+        ?><optgroup label="<?php echo Format::htmlchars($group); ?>"><?php
+    }
+    foreach ($actions as $type=>$name) {
+?>
+                <option data-title="<?php echo $name; ?>" value="<?php echo $type; ?>"
+                    data-multi-use="<?php echo $mu = FilterAction::lookupByType($type)->hasFlag(TriggerAction::FLAG_MULTI_USE); ?> " <?php
+                    if (in_array($type, $existing) && !$mu) echo 'disabled="disabled"';
+                ?>><?php echo $name; ?></option>
+<?php }
+} ?>
                 </select>
-                &nbsp;<span class="error"><?php echo $errors['topic_id']; ?></span><i class="help-tip icon-question-sign" href="#help_topic"></i>
+                <input id="new-action-btn" type="button" value="<?php echo __('Add'); ?>"
+                onclick="javascript:
+        var dropdown = $('#new-action-select'), selected = dropdown.find(':selected');
+        dropdown.val('');
+        $('#dynamic-actions')
+          .append($('<tr></tr>')
+            .append($('<td></td>')
+              .text(selected.data('title') + ':')
+            ).append($('<td></td>')
+              .append($('<em></em>').text(__('Loading ...')))
+              .load('ajax.php/filter/action/' + selected.val() + '/config', function() {
+                if (!selected.data('multiUse')) selected.prop('disabled', true);
+              })
+            )
+          ).append(
+            $('<input>').attr({type:'hidden',name:'actions[]',value:'N'+selected.val()})
+          );"/>
             </td>
         </tr>
         <tr>
@@ -392,3 +270,12 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
     <input type="button" name="cancel" value="<?php echo __('Cancel');?>" onclick='window.location.href="filters.php"'>
 </p>
 </form>
+<script type="text/javascript">
+   var fixHelper = function(e, ui) {
+      ui.children().each(function() {
+          $(this).width($(this).width());
+      });
+      return ui;
+   };
+   $('#dynamic-actions').sortable({helper: fixHelper, opacity: 0.5});
+</script>
diff --git a/include/staff/helptopic.inc.php b/include/staff/helptopic.inc.php
index 6fb6371bc77cd7a61e03ee1830c950cf2ea72504..cf43338fa5abf33fc15477418cc5d485d6a48653 100644
--- a/include/staff/helptopic.inc.php
+++ b/include/staff/helptopic.inc.php
@@ -10,6 +10,7 @@ if($topic && $_REQUEST['a']!='add') {
     $info['pid']=$topic->getPid();
     $trans['name'] = $topic->getTranslateTag('name');
     $qs += array('id' => $topic->getId());
+    $forms = $topic->getForms();
 } else {
     $title=__('Add New Help Topic');
     $action='create';
@@ -21,22 +22,31 @@ if($topic && $_REQUEST['a']!='add') {
 }
 $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
 ?>
+
+<h2 style="font-weight: normal"><?php echo $title; ?>
+    &nbsp;<i class="help-tip icon-question-sign" href="#help_topic_information"></i>
+    </h2>
+<?php if ($topic) { ?>
+    <div class="big"><strong><?php echo $topic->getLocal('topic'); ?></strong></div>
+<?php } ?>
+
+<br/>
+
+<ul class="tabs" id="topic-tabs">
+    <li class="active"><a href="#info"><i class="icon-info-sign"></i> Help Topic Information</a></li>
+    <li><a href="#routing"><i class="icon-ticket"></i> New ticket options</a></li>
+    <li><a href="#forms"><i class="icon-paste"></i> Forms</a></li>
+</ul>
+
 <form action="helptopics.php?<?php echo Http::build_query($qs); ?>" method="post" id="save">
  <?php csrf_token(); ?>
  <input type="hidden" name="do" value="<?php echo $action; ?>">
  <input type="hidden" name="a" value="<?php echo Format::htmlchars($_REQUEST['a']); ?>">
  <input type="hidden" name="id" value="<?php echo $info['id']; ?>">
- <h2><?php echo __('Help Topic');?></h2>
- <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2">
-    <thead>
-        <tr>
-            <th colspan="2">
-                <h4><?php echo $title; ?></h4>
-                <em><?php echo __('Help Topic Information');?>
-                &nbsp;<i class="help-tip icon-question-sign" href="#help_topic_information"></i></em>
-            </th>
-        </tr>
-    </thead>
+
+<div id="topic-tabs_container">
+<div class="tab_content" id="info">
+ <table class="table" border="0" cellspacing="0" cellpadding="2">
     <tbody>
         <tr>
             <td width="180" class="required">
@@ -53,8 +63,8 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
                 <?php echo __('Status');?>:
             </td>
             <td>
-                <input type="radio" name="isactive" value="1" <?php echo $info['isactive']?'checked="checked"':''; ?>><?php echo __('Active'); ?>
-                <input type="radio" name="isactive" value="0" <?php echo !$info['isactive']?'checked="checked"':''; ?>><?php echo __('Disabled'); ?>
+                <input type="radio" name="isactive" value="1" <?php echo $info['isactive']?'checked="checked"':''; ?>> <?php echo __('Active'); ?>
+                <input type="radio" name="isactive" value="0" <?php echo !$info['isactive']?'checked="checked"':''; ?>> <?php echo __('Disabled'); ?>
                 &nbsp;<span class="error">*&nbsp;</span> <i class="help-tip icon-question-sign" href="#status"></i>
             </td>
         </tr>
@@ -63,8 +73,8 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
                 <?php echo __('Type');?>:
             </td>
             <td>
-                <input type="radio" name="ispublic" value="1" <?php echo $info['ispublic']?'checked="checked"':''; ?>><?php echo __('Public'); ?>
-                <input type="radio" name="ispublic" value="0" <?php echo !$info['ispublic']?'checked="checked"':''; ?>><?php echo __('Private/Internal'); ?>
+                <input type="radio" name="ispublic" value="1" <?php echo $info['ispublic']?'checked="checked"':''; ?>> <?php echo __('Public'); ?>
+                <input type="radio" name="ispublic" value="0" <?php echo !$info['ispublic']?'checked="checked"':''; ?>> <?php echo __('Private/Internal'); ?>
                 &nbsp;<span class="error">*&nbsp;</span> <i class="help-tip icon-question-sign" href="#type"></i>
             </td>
         </tr>
@@ -87,28 +97,26 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
             </td>
         </tr>
 
-        <tr><th colspan="2"><em><?php echo __('New ticket options');?></em></th></tr>
-        <tr>
-            <td><strong><?php echo __('Custom Form'); ?></strong>:</td>
-           <td><select name="form_id">
-                <option value="0" <?php
-if ($info['form_id'] == '0') echo 'selected="selected"';
-                    ?>>&mdash; <?php echo __('None'); ?> &mdash;</option>
-                <option value="<?php echo Topic::FORM_USE_PARENT; ?>"  <?php
-if ($info['form_id'] == Topic::FORM_USE_PARENT) echo 'selected="selected"';
-                    ?>>&mdash; <?php echo __('Use Parent Form'); ?> &mdash;</option>
-               <?php foreach (DynamicForm::objects()->filter(array('type'=>'G')) as $group) { ?>
-                <option value="<?php echo $group->get('id'); ?>"
-                       <?php if ($group->get('id') == $info['form_id'])
-                            echo 'selected="selected"'; ?>>
-                       <?php echo $group->get('title'); ?>
-                   </option>
-               <?php } ?>
-               </select>
-               &nbsp;<span class="error">&nbsp;<?php echo $errors['form_id']; ?></span>
-               <i class="help-tip icon-question-sign" href="#custom_form"></i>
-           </td>
-        </tr>
+    </tbody>
+    </table>
+
+        <div style="padding:8px 3px;border-bottom: 2px dotted #ddd;">
+            <strong class="big"><?php echo __('Internal Notes');?></strong><br/>
+            <?php echo __("be liberal, they're internal.");?>
+        </div>
+
+        <textarea class="richtext no-bar" name="notes" cols="21"
+            rows="8" style="width: 80%;"><?php echo $info['notes']; ?></textarea>
+
+</div>
+
+<div class="hidden tab_content" id="routing">
+<div style="padding:8px 0;border-bottom: 2px dotted #ddd;">
+<div><b class="big"><?php echo __('New ticket options');?></b></div>
+</div>
+
+ <table class="table" border="0" cellspacing="0" cellpadding="2">
+        <tbody>
         <tr>
             <td width="180" class="required">
                 <?php echo __('Department'); ?>:
@@ -127,6 +135,62 @@ if ($info['form_id'] == Topic::FORM_USE_PARENT) echo 'selected="selected"';
                 <i class="help-tip icon-question-sign" href="#department"></i>
             </td>
         </tr>
+        <tr class="border">
+            <td>
+                <?php echo __('Ticket Number Format'); ?>:
+            </td>
+            <td>
+                <label>
+                <input type="radio" name="custom-numbers" value="0" <?php echo !$info['custom-numbers']?'checked="checked"':''; ?>
+                    onchange="javascript:$('#custom-numbers').hide();"> <?php echo __('System Default'); ?>
+                </label>&nbsp;<label>
+                <input type="radio" name="custom-numbers" value="1" <?php echo $info['custom-numbers']?'checked="checked"':''; ?>
+                    onchange="javascript:$('#custom-numbers').show(200);"> <?php echo __('Custom'); ?>
+                </label>&nbsp; <i class="help-tip icon-question-sign" href="#custom_numbers"></i>
+            </td>
+        </tr>
+    </tbody>
+    <tbody id="custom-numbers" style="<?php if (!$info['custom-numbers']) echo 'display:none'; ?>">
+        <tr>
+            <td style="padding-left:20px">
+                <?php echo __('Format'); ?>:
+            </td>
+            <td>
+                <input type="text" name="number_format" value="<?php echo $info['number_format']; ?>"/>
+                <span class="faded"><?php echo __('e.g.'); ?> <span id="format-example"><?php
+                    if ($info['custom-numbers']) {
+                        if ($info['sequence_id'])
+                            $seq = Sequence::lookup($info['sequence_id']);
+                        if (!isset($seq))
+                            $seq = new RandomSequence();
+                        echo $seq->current($info['number_format']);
+                    } ?></span></span>
+                <div class="error"><?php echo $errors['number_format']; ?></div>
+            </td>
+        </tr>
+        <tr>
+<?php $selected = 'selected="selected"'; ?>
+            <td style="padding-left:20px">
+                <?php echo __('Sequence'); ?>:
+            </td>
+            <td>
+                <select name="sequence_id">
+                <option value="0" <?php if ($info['sequence_id'] == 0) echo $selected;
+                    ?>>&mdash; <?php echo __('Random'); ?> &mdash;</option>
+<?php foreach (Sequence::objects() as $s) { ?>
+                <option value="<?php echo $s->id; ?>" <?php
+                    if ($info['sequence_id'] == $s->id) echo $selected;
+                    ?>><?php echo $s->name; ?></option>
+<?php } ?>
+                </select>
+                <button class="action-button pull-right" onclick="javascript:
+                $.dialog('ajax.php/sequence/manage', 205);
+                return false;
+                "><i class="icon-gear"></i> <?php echo __('Manage'); ?></button>
+            </td>
+        </tr>
+    </tbody>
+    <tbody>
         <tr>
             <td width="180">
                 <?php echo __('Status'); ?>:
@@ -266,75 +330,95 @@ if ($info['form_id'] == Topic::FORM_USE_PARENT) echo 'selected="selected"';
                     <i class="help-tip icon-question-sign" href="#ticket_auto_response"></i>
             </td>
         </tr>
-        <tr>
-            <td>
-                <?php echo __('Ticket Number Format'); ?>:
-            </td>
-            <td>
-                <label>
-                <input type="radio" name="custom-numbers" value="0" <?php echo !$info['custom-numbers']?'checked="checked"':''; ?>
-                    onchange="javascript:$('#custom-numbers').hide();"> <?php echo __('System Default'); ?>
-                </label>&nbsp;<label>
-                <input type="radio" name="custom-numbers" value="1" <?php echo $info['custom-numbers']?'checked="checked"':''; ?>
-                    onchange="javascript:$('#custom-numbers').show(200);"> <?php echo __('Custom'); ?>
-                </label>&nbsp; <i class="help-tip icon-question-sign" href="#custom_numbers"></i>
-            </td>
-        </tr>
     </tbody>
-    <tbody id="custom-numbers" style="<?php if (!$info['custom-numbers']) echo 'display:none'; ?>">
-        <tr>
-            <td style="padding-left:20px">
-                <?php echo __('Format'); ?>:
-            </td>
-            <td>
-                <input type="text" name="number_format" value="<?php echo $info['number_format']; ?>"/>
-                <span class="faded"><?php echo __('e.g.'); ?> <span id="format-example"><?php
-                    if ($info['custom-numbers']) {
-                        if ($info['sequence_id'])
-                            $seq = Sequence::lookup($info['sequence_id']);
-                        if (!isset($seq))
-                            $seq = new RandomSequence();
-                        echo $seq->current($info['number_format']);
-                    } ?></span></span>
-                <div class="error"><?php echo $errors['number_format']; ?></div>
-            </td>
-        </tr>
+ </table>
+</div>
+
+<div class="hidden tab_content" id="forms">
+ <table id="topic-forms" class="table" border="0" cellspacing="0" cellpadding="2">
+
+<?php
+$current_forms = array();
+foreach ($forms as $F) {
+    $current_forms[] = $F->id; ?>
+    <tbody data-form-id="<?php echo $F->get('id'); ?>">
         <tr>
-<?php $selected = 'selected="selected"'; ?>
-            <td style="padding-left:20px">
-                <?php echo __('Sequence'); ?>:
-            </td>
-            <td>
-                <select name="sequence_id">
-                <option value="0" <?php if ($info['sequence_id'] == 0) echo $selected;
-                    ?>>&mdash; <?php echo __('Random'); ?> &mdash;</option>
-<?php foreach (Sequence::objects() as $s) { ?>
-                <option value="<?php echo $s->id; ?>" <?php
-                    if ($info['sequence_id'] == $s->id) echo $selected;
-                    ?>><?php echo $s->name; ?></option>
+            <td class="handle" colspan="6">
+                <input type="hidden" name="forms[]" value="<?php echo $F->get('id'); ?>" />
+                <div class="pull-right">
+                <i class="icon-large icon-move icon-muted"></i>
+<?php if ($F->get('type') != 'T') { ?>
+                <a href="#" title="<?php echo __('Delete'); ?>" onclick="javascript:
+                if (confirm(__('You sure?')))
+                    var tbody = $(this).closest('tbody');
+                    tbody.fadeOut(function(){this.remove()});
+                    $(this).closest('form')
+                        .find('[name=form_id] [value=' + tbody.data('formId') + ']')
+                        .prop('disabled', false);
+                return false;"><i class="icon-large icon-trash"></i></a>
 <?php } ?>
-                </select>
-                <button class="action-button pull-right" onclick="javascript:
-                $.dialog('ajax.php/sequence/manage', 205);
-                return false;
-                "><i class="icon-gear"></i> <?php echo __('Manage'); ?></button>
+                </div>
+                <div><strong><?php echo Format::htmlchars($F->getLocal('title')); ?></strong></div>
+                <div><?php echo Format::display($F->getLocal('instructions')); ?></div>
             </td>
         </tr>
-    </tbody>
-    <tbody>
         <tr>
-            <th colspan="2">
-                <em><strong><?php echo __('Internal Notes');?></strong>: <?php echo __("be liberal, they're internal.");?></em>
-            </th>
+            <th><?php echo __('Enable'); ?></th>
+            <th><?php echo __('Label'); ?></th>
+            <th><?php echo __('Type'); ?></th>
+            <th><?php echo __('Visibility'); ?></th>
+            <th><?php echo __('Variable'); ?></th>
         </tr>
+    <?php
+        foreach ($F->getFields() as $f) { ?>
         <tr>
-            <td colspan=2>
-                <textarea class="richtext no-bar" name="notes" cols="21"
-                    rows="8" style="width: 80%;"><?php echo $info['notes']; ?></textarea>
-            </td>
+            <td><input type="checkbox" name="fields[]" value="<?php
+                echo $f->get('id'); ?>" <?php
+                if ($f->isEnabled()) echo 'checked="checked"'; ?>/></td>
+            <td><?php echo $f->get('label'); ?></td>
+            <td><?php $t=FormField::getFieldType($f->get('type')); echo __($t[0]); ?></td>
+            <td><?php echo $f->getVisibilityDescription(); ?></td>
+            <td><?php echo $f->get('name'); ?></td>
         </tr>
+        <?php } ?>
     </tbody>
-</table>
+    <?php } ?>
+ </table>
+
+   <br/>
+   <strong><?php echo __('Add Custom Form'); ?></strong>:
+   <select name="form_id" onchange="javascript:
+    event.preventDefault();
+    var $this = $(this),
+        val = $this.val();
+    if (!val) return;
+    $.ajax({
+        url: 'ajax.php/form/' + val + '/fields/view',
+        dataType: 'json',
+        success: function(json) {
+            if (json.success) {
+                $(json.html).appendTo('#topic-forms').effect('highlight');
+                $this.find(':selected').prop('disabled', true);
+            }
+        }
+    });">
+    <option value=""><?php echo '— '.__('Add a custom form') . ' —'; ?></option>
+    <?php foreach (DynamicForm::objects()->filter(array('type'=>'G')) as $F) { ?>
+        <option value="<?php echo $F->get('id'); ?>"
+           <?php if (in_array($F->id, $current_forms))
+               echo 'disabled="disabled"'; ?>
+           <?php if ($F->get('id') == $info['form_id'])
+                echo 'selected="selected"'; ?>>
+           <?php echo $F->getLocal('title'); ?>
+        </option>
+    <?php } ?>
+   </select>
+   &nbsp;<span class="error">&nbsp;<?php echo $errors['form_id']; ?></span>
+   <i class="help-tip icon-question-sign" href="#custom_form"></i>
+</div>
+
+</div>
+
 <p style="text-align:center;">
     <input type="submit" name="submit" value="<?php echo $submit_text; ?>">
     <input type="reset"  name="reset"  value="<?php echo __('Reset');?>">
@@ -355,4 +439,19 @@ $(function() {
     $('[name=sequence_id]').on('change', update_example);
     $('[name=number_format]').on('keyup', update_example);
 });
+$('table#topic-forms').sortable({
+  items: 'tbody',
+  handle: 'td.handle',
+  tolerance: 'pointer',
+  forcePlaceholderSize: true,
+  helper: function(e, ui) {
+    ui.children().each(function() {
+      $(this).children().each(function() {
+        $(this).width($(this).width());
+      });
+    });
+    ui=ui.clone().css({'background-color':'white', 'opacity':0.8});
+    return ui;
+  }
+}).disableSelection();
 </script>
diff --git a/include/staff/page.inc.php b/include/staff/page.inc.php
index cbc2d2526f1ed37e65952a3efb4e309208825c63..6326500aba5bd1b6d545eb1f8eae59bb7645cbda 100644
--- a/include/staff/page.inc.php
+++ b/include/staff/page.inc.php
@@ -116,10 +116,11 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
                     <li><a href="#notes"><?php echo __('Internal Notes'); ?></a></li>
                 </ul>
     <div class="tab_content active" id="content">
+<table class="full-width"><tbody><tr><td style="vertical-align:top">
 <?php
 $langs = Internationalization::getConfiguredSystemLanguages();
 if ($page && count($langs) > 1) { ?>
-    <ul class="vertical left tabs">
+    <ul class="vertical tabs" id="translations">
         <li class="empty"><i class="icon-globe" title="This content is translatable"></i></li>
 <?php foreach ($langs as $tag=>$nfo) { ?>
     <li class="<?php if ($tag == $cfg->getPrimaryLanguage()) echo "active";
@@ -131,15 +132,18 @@ if ($page && count($langs) > 1) { ?>
     </ul>
 <?php
 } ?>
-    <div id="msg_info" style="margin:0 55px">
+</td>
+<td id="translations_container" style="padding-left: 10px">
+    <div id="msg_info">
     <em><i class="icon-info-sign"></i> <?php
         echo __(
             'Ticket variables are only supported in thank-you pages.'
-    ); ?></em></div>
+        ); ?></em>
+    </div>
 
-        <div id="translation-<?php echo $cfg->getPrimaryLanguage(); ?>" class="tab_content" style="margin:0 45px"
+        <div id="translation-<?php echo $cfg->getPrimaryLanguage(); ?>" class="tab_content"
             lang="<?php echo $cfg->getPrimaryLanguage(); ?>">
-        <textarea name="body" cols="21" rows="12" style="width:98%;" class="richtext draft"
+        <textarea name="body" cols="21" rows="12" style="width:100%" class="richtext draft"
 <?php
     list($draft, $attrs) = Draft::getDraftAndDataAttrs('page', $info['id'], $info['body']);
     echo $attrs; ?>><?php echo $draft ?: $info['body']; ?></textarea>
@@ -150,17 +154,18 @@ if ($page && count($langs) > 1) { ?>
         if ($tag == $cfg->getPrimaryLanguage())
             continue; ?>
         <div id="translation-<?php echo $tag; ?>" class="tab_content"
-            style="display:none;margin:0 45px" lang="<?php echo $tag; ?>">
+            style="display:none;" lang="<?php echo $tag; ?>">
         <textarea name="trans[<?php echo $tag; ?>][body]" cols="21" rows="12"
-            style="width:98%;" class="richtext draft"
+            style="width:100%" class="richtext draft"
 <?php
     list($draft, $attrs) = Draft::getDraftAndDataAttrs('page', $info['id'].'.'.$tag, $info['trans'][$tag]);
     echo $attrs; ?>><?php echo $draft ?: $info['trans'][$tag]; ?></textarea>
         </div>
 <?php }
 } ?>
+</td></tr></tbody></table>
 
-        <div class="error" style="margin: 5px 55px"><?php echo $errors['body']; ?></div>
+        <div class="error" style="margin: 5px 0"><?php echo $errors['body']; ?></div>
         <div class="clear"></div>
     </div>
     <div class="tab_content" style="display:none" id="notes">
diff --git a/include/staff/templates/dynamic-form-simple.tmpl.php b/include/staff/templates/dynamic-form-simple.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..1c31b8fed135211e77fe932922e44f989cabaeda
--- /dev/null
+++ b/include/staff/templates/dynamic-form-simple.tmpl.php
@@ -0,0 +1,32 @@
+        <?php
+        echo $form->getMedia();
+        foreach ($form->getFields() as $name=>$f) { ?>
+            <div class="flush-left custom-field" id="field<?php echo $f->getWidget()->id;
+                ?>" <?php if (!$f->isVisible()) echo 'style="display:none;"'; ?>>
+            <div class="field-label <?php if ($f->get('required')) echo 'required'; ?>">
+            <label for="<?php echo $f->getWidget()->name; ?>">
+      <?php if ($f->get('label')) { ?>
+                <?php echo Format::htmlchars($f->get('label')); ?>:
+      <?php } ?>
+      <?php if ($f->get('required')) { ?>
+                <span class="error">*</span>
+      <?php } ?>
+            </label>
+            <?php
+            if ($f->get('hint')) { ?>
+                <br/><em style="color:gray;display:inline-block"><?php
+                    echo Format::htmlchars($f->get('hint')); ?></em>
+            <?php
+            } ?>
+            </div><div>
+            <?php
+            $f->render();
+            ?>
+            </div>
+            <?php
+            foreach ($f->errors() as $e) { ?>
+                <div class="error"><?php echo $e; ?></div>
+            <?php } ?>
+            </div>
+        <?php }
+        ?>
diff --git a/include/staff/templates/dynamic-form.tmpl.php b/include/staff/templates/dynamic-form.tmpl.php
index 487ce9a56f863a735abe6808f29a95c56bf2dc9f..5002700fce79396f2f86098a030ac1c061cdb357 100644
--- a/include/staff/templates/dynamic-form.tmpl.php
+++ b/include/staff/templates/dynamic-form.tmpl.php
@@ -20,8 +20,7 @@ if (isset($options['entry']) && $options['mode'] == 'edit') { ?>
 <?php } ?>
 <?php if ($form->getTitle()) { ?>
     <tr><th colspan="2">
-        <em><strong><?php echo Format::htmlchars($form->getTitle()); ?></strong>:
-        <?php echo Format::htmlchars($form->getInstructions()); ?>
+        <em>
 <?php if ($options['mode'] == 'edit') { ?>
         <div class="pull-right">
     <?php if ($options['entry']
@@ -32,12 +31,17 @@ if (isset($options['entry']) && $options['mode'] == 'edit') { ?>
     <?php } ?>
             <i class="icon-sort" title="Drag to Sort"></i>
         </div>
-<?php } ?></em>
+<?php } ?>
+        <strong><?php echo Format::htmlchars($form->getTitle()); ?></strong>:
+        <div><?php echo Format::display($form->getInstructions()); ?></div>
+        </em>
     </th></tr>
     <?php
     }
     foreach ($form->getFields() as $field) {
         try {
+            if (!$field->isEnabled())
+                continue;
             if ($options['mode'] == 'edit' && !$field->isEditableToStaff())
                 continue;
         }
diff --git a/include/staff/ticket-open.inc.php b/include/staff/ticket-open.inc.php
index fcebc13d474f66469e7eb2e188f55505de61e165..e9edb4643ade0c9aa8dbf48f3b3c498c641a4ddc 100644
--- a/include/staff/ticket-open.inc.php
+++ b/include/staff/ticket-open.inc.php
@@ -9,12 +9,14 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
 if (!$info['topicId'])
     $info['topicId'] = $cfg->getDefaultTopicId();
 
-$form = null;
+$forms = array();
 if ($info['topicId'] && ($topic=Topic::lookup($info['topicId']))) {
-    $form = $topic->getForm();
-    if ($_POST && $form) {
-        $form = $form->instanciate();
-        $form->isValid();
+    foreach ($topic->getForms() as $F) {
+        if ($_POST) {
+            $F = $F->instanciate();
+            $F->isValidForClient();
+        }
+        $forms[] = $F;
     }
 }
 
@@ -156,9 +158,9 @@ if ($_POST)
                                 $id, ($info['topicId']==$id)?'selected="selected"':'',
                                 $selected, $name);
                         }
-                        if (count($topics) == 1 && !$form) {
+                        if (count($topics) == 1 && !$forms) {
                             if (($T = Topic::lookup($id)))
-                                $form =  $T->getForm();
+                                $forms =  $T->getForms();
                         }
                     }
                     ?>
@@ -260,18 +262,21 @@ if ($_POST)
         </tbody>
         <tbody id="dynamic-form">
         <?php
-            if ($form) {
+            foreach ($forms as $form) {
+                $hasFields = false;
+                foreach ($form->getFields() as $f) {
+                    if ($f->isVisibleToStaff()) {
+                        $hasFields = true;
+                        break;
+                    }
+                }
+                if (!$hasFields)
+                    continue;
                 print $form->getForm()->getMedia();
                 include(STAFFINC_DIR .  'templates/dynamic-form.tmpl.php');
             }
         ?>
         </tbody>
-        <tbody> <?php
-        $tform = TicketForm::getInstance()->getForm($_POST);
-        if ($_POST) $tform->isValid();
-        $tform->render(true);
-        ?>
-        </tbody>
         <tbody>
         <?php
         //is the user allowed to post replies??
diff --git a/include/staff/ticket-view.inc.php b/include/staff/ticket-view.inc.php
index de9ccf80153ce21876688d343459a1cb08dc6b17..5bda5f6374998dd31bf662c8fe8a37b3e8375eb1 100644
--- a/include/staff/ticket-view.inc.php
+++ b/include/staff/ticket-view.inc.php
@@ -466,7 +466,7 @@ $tcount+= $ticket->getNumNotes();
             $msgId = $entry['id'];
        }
     } else {
-        echo '<p>'.__('Error fetching ticket thread - get technical help.').'</p>';
+        echo '<p><em>'.__('No entries have been posted to this ticket.').'</em></p>';
     }?>
 <div class="clear" style="padding-bottom:10px;"></div>
 <?php if($errors['err']) { ?>
diff --git a/include/upgrader/streams/core.sig b/include/upgrader/streams/core.sig
index 209fca107da75265e35d6cd2f12d71d5e89213a5..3cbda6fa0ad2b31ab58b8b2c3201f386774c454a 100644
--- a/include/upgrader/streams/core.sig
+++ b/include/upgrader/streams/core.sig
@@ -1 +1 @@
-5cd0a25a54fd27ed95f00d62edda4c6d
+a22c2b4ff54ce5aa61e94124a73e6eac
diff --git a/include/upgrader/streams/core/5cd0a25a-a22c2b4f.cleanup.sql b/include/upgrader/streams/core/5cd0a25a-a22c2b4f.cleanup.sql
new file mode 100644
index 0000000000000000000000000000000000000000..dd4a3f887dee2d7a24c557287cf94052c4fd2640
--- /dev/null
+++ b/include/upgrader/streams/core/5cd0a25a-a22c2b4f.cleanup.sql
@@ -0,0 +1,21 @@
+/**
+ * @signature 0e47d678f50874fa0d33e1e3759f657e
+ * @version v1.9.6
+ * @title Make fields disable-able per help topic
+ */
+
+ALTER TABLE `%TABLE_PREFIX%help_topic`
+    DROP `form_id` int(10) unsigned NOT NULL default '0';
+
+ALTER TABLE `%TABLE_PREFIX%filter`
+  DROP `reject_ticket`,
+  DROP `use_replyto_email`,
+  DROP `disable_autoresponder`,
+  DROP `canned_response_id`,
+  DROP `status_id`,
+  DROP `priority_id`,
+  DROP `dept_id`,
+  DROP `staff_id`,
+  DROP `team_id`,
+  DROP `sla_id`,
+  DROP `form_id`;
diff --git a/include/upgrader/streams/core/5cd0a25a-a22c2b4f.patch.sql b/include/upgrader/streams/core/5cd0a25a-a22c2b4f.patch.sql
new file mode 100644
index 0000000000000000000000000000000000000000..509c83519cff637457c87a20df6b68c1db00de6a
--- /dev/null
+++ b/include/upgrader/streams/core/5cd0a25a-a22c2b4f.patch.sql
@@ -0,0 +1,138 @@
+/**
+ * @signature a22c2b4ff54ce5aa61e94124a73e6eac
+ * @version v1.9.6
+ * @title Make fields disable-able per help topic
+ *
+ * This patch adds the ability to associate more than one extra form with a
+ * help topic, allows specifying the sort order of each form, including the
+ * main ticket details forms, and also allows disabling any of the fields on
+ * any of the associated forms, including the issue details field.
+ *
+ * This patch migrates the columnar layout of the %filter table into a new
+ * %filter_action table. The cleanup portion of the script will drop the old
+ * columns from the %filter table.
+ */
+
+DROP TABLE IF EXISTS `%TABLE_PREFIX%filter_action`;
+CREATE TABLE `%TABLE_PREFIX%filter_action` (
+  `id` int(11) unsigned NOT NULL auto_increment,
+  `filter_id` int(10) unsigned NOT NULL,
+  `sort` int(10) unsigned NOT NULL default 0,
+  `type` varchar(24) NOT NULL,
+  `configuration` text,
+  `updated` datetime NOT NULL,
+  PRIMARY KEY (`id`),
+  KEY `filter_id` (`filter_id`)
+) DEFAULT CHARSET=utf8;
+
+INSERT INTO `%TABLE_PREFIX%filter_action`
+    (`filter_id`, `type`, `configuration`, `updated`)
+    SELECT `id`, 'reject', '', `updated`
+    FROM `%TABLE_PREFIX%filter`
+    WHERE `reject_ticket` != 0;
+
+INSERT INTO `%TABLE_PREFIX%filter_action`
+    (`filter_id`, `type`, `configuration`, `updated`)
+    SELECT `id`, 'replyto', '{"enable":true}', `updated`
+    FROM `%TABLE_PREFIX%filter`
+    WHERE `use_replyto_email` != 0;
+
+INSERT INTO `%TABLE_PREFIX%filter_action`
+    (`filter_id`, `type`, `configuration`, `updated`)
+    SELECT `id`, 'noresp', '{"enable":true}', `updated`
+    FROM `%TABLE_PREFIX%filter`
+    WHERE `disable_autoresponder` != 0;
+
+INSERT INTO `%TABLE_PREFIX%filter_action`
+    (`filter_id`, `type`, `configuration`, `updated`)
+    SELECT `id`, 'canned', CONCAT('{"canned_id":',`canned_response_id`,'}'), `updated`
+    FROM `%TABLE_PREFIX%filter`
+    WHERE `canned_response_id` != 0;
+
+INSERT INTO `%TABLE_PREFIX%filter_action`
+    (`filter_id`, `type`, `configuration`, `updated`)
+    SELECT `id`, 'dept', CONCAT('{"dept_id":',`dept_id`,'}'), `updated`
+    FROM `%TABLE_PREFIX%filter`
+    WHERE `dept_id` != 0;
+
+INSERT INTO `%TABLE_PREFIX%filter_action`
+    (`filter_id`, `type`, `configuration`, `updated`)
+    SELECT `id`, 'pri', CONCAT('{"priority_id":',`priority_id`,'}'), `updated`
+    FROM `%TABLE_PREFIX%filter`
+    WHERE `priority_id` != 0;
+
+INSERT INTO `%TABLE_PREFIX%filter_action`
+    (`filter_id`, `type`, `configuration`, `updated`)
+    SELECT `id`, 'sla', CONCAT('{"sla_id":',`sla_id`,'}'), `updated`
+    FROM `%TABLE_PREFIX%filter`
+    WHERE `sla_id` != 0;
+
+INSERT INTO `%TABLE_PREFIX%filter_action`
+    (`filter_id`, `type`, `configuration`, `updated`)
+    SELECT `id`, 'team', CONCAT('{"team_id":',`team_id`,'}'), `updated`
+    FROM `%TABLE_PREFIX%filter`
+    WHERE `team_id` != 0;
+
+INSERT INTO `%TABLE_PREFIX%filter_action`
+    (`filter_id`, `type`, `configuration`, `updated`)
+    SELECT `id`, 'agent', CONCAT('{"staff_id":',`staff_id`,'}'), `updated`
+    FROM `%TABLE_PREFIX%filter`
+    WHERE `staff_id` != 0;
+
+INSERT INTO `%TABLE_PREFIX%filter_action`
+    (`filter_id`, `type`, `configuration`, `updated`)
+    SELECT `id`, 'topic', CONCAT('{"topic_id":',`topic_id`,'}'), `updated`
+    FROM `%TABLE_PREFIX%filter`
+    WHERE `topic_id` != 0;
+
+INSERT INTO `%TABLE_PREFIX%filter_action`
+    (`filter_id`, `type`, `configuration`, `updated`)
+    SELECT `id`, 'status', CONCAT('{"status_id":',`status_id`,'}'), `updated`
+    FROM `%TABLE_PREFIX%filter`
+    WHERE `status_id` != 0;
+
+ALTER TABLE `%TABLE_PREFIX%form`
+    ADD `pid` int(10) unsigned DEFAULT NULL AFTER `id`,
+    ADD `name` varchar(64) NOT NULL DEFAULT '' AFTER `instructions`;
+
+ALTER TABLE `%TABLE_PREFIX%form_entry`
+    ADD `extra` text AFTER `sort`;
+
+CREATE TABLE `%TABLE_PREFIX%help_topic_form` (
+  `id` int(11) unsigned NOT NULL auto_increment,
+  `topic_id` int(11) unsigned NOT NULL default 0,
+  `form_id` int(10) unsigned NOT NULL default 0,
+  `sort` int(10) unsigned NOT NULL default 1,
+  `extra` text,
+  PRIMARY KEY  (`topic_id`, `form_id`)
+) DEFAULT CHARSET=utf8;
+
+-- Handle A4 / A3 / A2 / A1 help topics. For these, consider the forms
+-- associated with each, which should sort above the ticket details form, as
+-- the graphical interface rendered it suchly. Then, consider cascaded
+-- forms, where the parent form was specified on a child.
+insert into `%TABLE_PREFIX%help_topic_form`
+    (`topic_id`, `form_id`, `sort`)
+    select A1.topic_id, case
+        when A3.form_id = 4294967295 then A4.form_id
+        when A2.form_id = 4294967295 then A3.form_id
+        when A1.form_id = 4294967295 then A2.form_id
+        else COALESCE(A4.form_id, A3.form_id, A2.form_id, A1.form_id) end as form_id, 1 as `sort`
+    from `%TABLE_PREFIX%help_topic` A1
+    left join `%TABLE_PREFIX%help_topic` A2 on (A2.topic_id = A1.topic_pid)
+    left join `%TABLE_PREFIX%help_topic` A3 on (A3.topic_id = A2.topic_pid)
+    left join `%TABLE_PREFIX%help_topic` A4 on (A4.topic_id = A3.topic_pid)
+    having `form_id` > 0
+    union
+    select A2.topic_id, id as `form_id`, 2 as `sort`
+    from `%TABLE_PREFIX%form` A1
+    join `%TABLE_PREFIX%help_topic` A2
+    where A1.`type` = 'T';
+
+ALTER TABLE `%TABLE_PREFIX%help_topic`
+    DROP `form_id` int(10) unsigned NOT NULL default '0';
+
+-- Finished with patch
+UPDATE `%TABLE_PREFIX%config`
+    SET `value` = 'a22c2b4ff54ce5aa61e94124a73e6eac'
+    WHERE `key` = 'schema_signature' AND `namespace` = 'core';
diff --git a/include/upgrader/streams/core/5cd0a25a-a22c2b4f.task.php b/include/upgrader/streams/core/5cd0a25a-a22c2b4f.task.php
new file mode 100644
index 0000000000000000000000000000000000000000..0379bd4becece7cb4cf4688acfc891b1c08098ca
--- /dev/null
+++ b/include/upgrader/streams/core/5cd0a25a-a22c2b4f.task.php
@@ -0,0 +1,14 @@
+<?php
+
+class InstructionPorter extends MigrationTask {
+    var $description = "Converting custom form instructions to HTML";
+
+    function run($max_time) {
+        foreach (DynamicForm::objects() as $F) {
+            $F->instructions = Format::htmlchars($F->get('instructions'));
+            $F->save();
+        }
+    }
+}
+
+return 'InstructionsPorter';
diff --git a/scp/ajax.php b/scp/ajax.php
index 131243adeb18c39b487631bcb9c5aee7b50044c1..140085b3910165086b67e1e9f4d61428777569e2 100644
--- a/scp/ajax.php
+++ b/scp/ajax.php
@@ -57,7 +57,11 @@ $dispatcher = patterns('',
         url_post('^field-config/(?P<id>\d+)$', 'saveFieldConfiguration'),
         url_delete('^answer/(?P<entry>\d+)/(?P<field>\d+)$', 'deleteAnswer'),
         url_post('^upload/(\d+)?$', 'upload'),
-        url_post('^upload/(\w+)?$', 'attach')
+        url_post('^upload/(\w+)?$', 'attach'),
+        url_get('^(?P<id>\d+)/fields/view$', 'getAllFields')
+    )),
+    url('^/filter/', patterns('ajax.filter.php:FilterAjaxAPI',
+        url_get('^action/(?P<type>\w+)/config$', 'getFilterActionForm')
     )),
     url('^/list/', patterns('ajax.forms.php:DynamicFormsAjaxAPI',
         url_get('^(?P<list>\w+)/item/(?P<id>\d+)/properties$', 'getListItemProperties'),
diff --git a/scp/css/scp.css b/scp/css/scp.css
index 0d76bf2ba467afd9ff21f10e99c9f039bdc3fb2e..ebb60c9ba34d2b21761a3fdb59f148524e7ef8cc 100644
--- a/scp/css/scp.css
+++ b/scp/css/scp.css
@@ -51,6 +51,10 @@ div#header a {
     clear:both;
 }
 
+.big {
+    font-size: 110%;
+}
+
 .faded {
     color:#666;
 }
@@ -589,6 +593,27 @@ a.print {
     padding:2px;
 }
 
+.table {
+    width: 100%;
+    border-collapse: collapse;
+    margin-top:3px;
+}
+
+.table tr.header td,
+.table th {
+    font-weight: bold;
+    text-align: left;
+    height: 24px;
+    background: #f0f0f0;
+}
+
+.table tr {
+    border-bottom:1px dotted #ddd;
+}
+.table td:not(:empty) {
+    height: 24px;
+}
+
 .form_table {
     margin-top:3px;
     border-left:1px solid #ddd;
@@ -607,10 +632,16 @@ table.fixed {
     border-collapse: collapse;
     width: 100%;
 }
-table.fixed td {
+table.fixed > thead > tr > th,
+table.fixed > thead > tr > td,
+table.fixed > tbody > tr > td,
+table.fixed > tr > td {
     width: 180px;
 }
-table.fixed td + td {
+table.fixed > thead > tr > th + th,
+table.fixed > thead > tr > td + td,
+table.fixed > tbody > tr > td + td,
+table.fixed > tr > td + td {
     width: auto;
 }
 
@@ -1459,12 +1490,12 @@ time {
     border:none;
 }
 
-.dialog .custom-field .field-label {
+.custom-field .field-label {
     margin-left: 3px;
     margin-right: 3px;
 }
-.dialog .custom-field + .custom-field {
-    margin-top: 8px;
+.custom-field + .custom-field {
+    margin-top: 5px;
 }
 .dialog label.fixed-size {
     width:100px;
@@ -2102,3 +2133,11 @@ td.indented {
    position: relative;
    top: -1px;
 }
+
+#topic-forms tbody + tbody td.handle {
+  padding-top: 15px;
+}
+
+#dynamic-actions > tr > td {
+    padding: 5px;
+}
diff --git a/scp/css/translatable.css b/scp/css/translatable.css
index d949d2d4fafac616e4fd4764847044e7116b90cf..3bece152ac700f39a6e7ff6404e3e7ae14814044 100644
--- a/scp/css/translatable.css
+++ b/scp/css/translatable.css
@@ -100,13 +100,14 @@ div.translatable {
   box-shadow: inset 0 1px 1px rgba(0,0,0,0.05);
   display: inline-block;
   white-space: nowrap;
-  border-top-left-radius: 4px;
-  border-bottom-left-radius: 4px;
+  border-top-left-radius: 3px;
+  border-bottom-left-radius: 3px;
   border-right: none;
-  padding: 0 5px 2px;
+  padding: 1px 5px 1px;
   margin-left: 2px;
   width: auto;
   background-color: white;
+  line-height: 16px;
 }
 div.translatable.textarea {
   border: 1px solid #bbb;
diff --git a/setup/inc/streams/core/install-mysql.sql b/setup/inc/streams/core/install-mysql.sql
index 124b65afbb7316e046f27a427bd569048d25a17f..d988d02b0c802d1f5ba76f71d4921283fb026c84 100644
--- a/setup/inc/streams/core/install-mysql.sql
+++ b/setup/inc/streams/core/install-mysql.sql
@@ -115,10 +115,12 @@ INSERT INTO `%TABLE_PREFIX%config` (`namespace`, `key`, `value`) VALUES
 DROP TABLE IF EXISTS `%TABLE_PREFIX%form`;
 CREATE TABLE `%TABLE_PREFIX%form` (
     `id` int(11) unsigned NOT NULL auto_increment,
+    `pid` int(10) unsigned DEFAULT NULL,
     `type` varchar(8) NOT NULL DEFAULT 'G',
     `deletable` tinyint(1) NOT NULL DEFAULT 1,
     `title` varchar(255) NOT NULL,
     `instructions` varchar(512),
+    `name` varchar(64) NOT NULL DEFAULT '',
     `notes` text,
     `created` datetime NOT NULL,
     `updated` datetime NOT NULL,
@@ -151,6 +153,7 @@ CREATE TABLE `%TABLE_PREFIX%form_entry` (
     `object_id` int(11) unsigned,
     `object_type` char(1) NOT NULL DEFAULT 'T',
     `sort` int(11) unsigned NOT NULL DEFAULT 1,
+    `extra` text,
     `created` datetime NOT NULL,
     `updated` datetime NOT NULL,
     PRIMARY KEY (`id`),
@@ -299,20 +302,6 @@ CREATE TABLE `%TABLE_PREFIX%filter` (
   `status` int(11) unsigned NOT NULL DEFAULT '0',
   `match_all_rules` tinyint(1) unsigned NOT NULL default '0',
   `stop_onmatch` tinyint(1) unsigned NOT NULL default '0',
-  `reject_ticket` tinyint(1) unsigned NOT NULL default '0',
-  `use_replyto_email` tinyint(1) unsigned NOT NULL default '0',
-  `disable_autoresponder` tinyint(1) unsigned NOT NULL default '0',
-  `canned_response_id` int(11) unsigned NOT NULL default '0',
-  `email_id` int(10) unsigned NOT NULL default '0',
-  `status_id` int(10) unsigned NOT NULL default '0',
-  `priority_id` int(10) unsigned NOT NULL default '0',
-  `dept_id` int(10) unsigned NOT NULL default '0',
-  `staff_id` int(10) unsigned NOT NULL default '0',
-  `team_id` int(10) unsigned NOT NULL default '0',
-  `sla_id` int(10) unsigned NOT NULL default '0',
-  `form_id` int(11) unsigned NOT NULL default '0',
-  `topic_id` int(11) unsigned NOT NULL default '0',
-  `ext_id` varchar(11),
   `target` ENUM(  'Any',  'Web',  'Email',  'API' ) NOT NULL DEFAULT  'Any',
   `name` varchar(32) NOT NULL default '',
   `notes` text,
@@ -323,6 +312,18 @@ CREATE TABLE `%TABLE_PREFIX%filter` (
   KEY `email_id` (`email_id`)
 ) DEFAULT CHARSET=utf8;
 
+DROP TABLE IF EXISTS `%TABLE_PREFIX%filter_action`;
+CREATE TABLE `%TABLE_PREFIX%filter_action` (
+  `id` int(11) unsigned NOT NULL auto_increment,
+  `filter_id` int(10) unsigned NOT NULL,
+  `sort` int(10) unsigned NOT NULL default 0,
+  `type` varchar(24) NOT NULL,
+  `configuration` text,
+  `updated` datetime NOT NULL,
+  PRIMARY KEY (`id`),
+  KEY `filter_id` (`filter_id`)
+) DEFAULT CHARSET=utf8;
+
 DROP TABLE IF EXISTS `%TABLE_PREFIX%filter_rule`;
 CREATE TABLE `%TABLE_PREFIX%filter_rule` (
   `id` int(11) unsigned NOT NULL auto_increment,
@@ -443,7 +444,6 @@ CREATE TABLE `%TABLE_PREFIX%help_topic` (
   `team_id` int(10) unsigned NOT NULL default '0',
   `sla_id` int(10) unsigned NOT NULL default '0',
   `page_id` int(10) unsigned NOT NULL default '0',
-  `form_id` int(10) unsigned NOT NULL default '0',
   `sequence_id` int(10) unsigned NOT NULL DEFAULT '0',
   `sort` int(10) unsigned NOT NULL default '0',
   `topic` varchar(32) NOT NULL default '',
@@ -461,6 +461,16 @@ CREATE TABLE `%TABLE_PREFIX%help_topic` (
   KEY `page_id` (`page_id`)
 ) DEFAULT CHARSET=utf8;
 
+DROP TABLE IF EXISTS `%TABLE_PREFIX%help_topic_form`;
+CREATE TABLE `%TABLE_PREFIX%help_topic_form` (
+  `id` int(11) unsigned NOT NULL auto_increment,
+  `topic_id` int(11) unsigned NOT NULL default 0,
+  `form_id` int(10) unsigned NOT NULL default 0,
+  `sort` int(10) unsigned NOT NULL default 1,
+  `extra` text,
+  PRIMARY KEY  (`topic_id`, `form_id`)
+) DEFAULT CHARSET=utf8;
+
 DROP TABLE IF EXISTS `%TABLE_PREFIX%organization`;
 CREATE TABLE `%TABLE_PREFIX%organization` (
   `id` int(11) unsigned NOT NULL AUTO_INCREMENT,