[Bps-public-commit] rt-extension-formtools branch dynamic-forms-from-config created. 0.53-84-g9e1a3d1

BPS Git Server git at git.bestpractical.com
Tue Oct 31 20:13:15 UTC 2023


This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "rt-extension-formtools".

The branch, dynamic-forms-from-config has been created
        at  9e1a3d1b50b9c557d619e7910c89cdc0e60d9e89 (commit)

- Log -----------------------------------------------------------------
commit 9e1a3d1b50b9c557d619e7910c89cdc0e60d9e89
Author: Jason Crome <jcrome at bestpractical.com>
Date:   Tue Oct 31 16:12:48 2023 -0400

    RT-Extension-FormTools 0.55

diff --git a/Changes b/Changes
index e0ab4af..e1b4cef 100644
--- a/Changes
+++ b/Changes
@@ -1,5 +1,18 @@
 Revision history for RT-Extension-FormTools
 
+0.55 2023-10-31
+ - Make core and custom fields consistent with other HTML elements
+ - Sync page name and tab text on name update
+ - Remove duplicate form tag
+ - Put Select and Create under one menu (for consistency)
+ - Make placeholders appear lighter than other text elements
+ - Validate errors in real time
+ - Fix HTML element IDs for custom fields
+ - On form edit page, show label text next to CF names
+ - Rename Save button on modal popups to Update
+ - Fix edit link for newly added items
+ - Center paragraph content on form pages
+
 0.54 2023-10-30
  - Create a graphical editor for forms
 
diff --git a/META.yml b/META.yml
index 5a9fcf9..e15058e 100644
--- a/META.yml
+++ b/META.yml
@@ -25,6 +25,6 @@ requires:
 resources:
   license: http://opensource.org/licenses/gpl-license.php
   repository: https://github.com/bestpractical/rt-extension-formtools
-version: '0.54'
+version: '0.55'
 x_module_install_rtx_version: '0.43'
 x_requires_rt: 5.0.0
diff --git a/lib/RT/Extension/FormTools.pm b/lib/RT/Extension/FormTools.pm
index 2af7c2a..c02a1eb 100644
--- a/lib/RT/Extension/FormTools.pm
+++ b/lib/RT/Extension/FormTools.pm
@@ -3,7 +3,7 @@ use strict;
 
 package RT::Extension::FormTools;
 
-our $VERSION = '0.54';
+our $VERSION = '0.55';
 
 RT->AddStyleSheets('rt-extension-formtools.css');
 RT->AddJavaScript('rt-extension-formtools.js');

commit 47c6e7f3f020cc3fa0a2806f92dd09541c9d9281
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Tue Oct 31 14:34:38 2023 -0400

    Add search support to source list
    
    This is a simplified version of RT core selectionbox's search feature.

diff --git a/html/Admin/FormTools/Modify.html b/html/Admin/FormTools/Modify.html
index ea53511..3593552 100644
--- a/html/Admin/FormTools/Modify.html
+++ b/html/Admin/FormTools/Modify.html
@@ -6,6 +6,13 @@
 <div class="row">
   <div class="formtools-component-menu boxcontainer col-md-3" id="formtools-component-wrapper">
     <&| /Widgets/TitleBox, title => loc('FormTools Components') &>
+      <div class="filters">
+        <div class="form-row">
+          <div class="col-12">
+            <input type="search" class="m-1 field form-control" name="search" placeholder="<&|/l&>Search...</&>" autocomplete="off">
+          </div>
+        </div>
+      </div>
       <div class="d-block text-center">
 % foreach my $item ( @html_components ) {
         <div id="formtools-element-<% $item %>" class="formtools-element" draggable="true" ondragstart="formTools.dragstart(event);" ondragend="formTools.dragend(event);" data-value="<% JSON({ type => 'raw_html', html => "<$item>test</$item>"}) %>">
@@ -401,6 +408,7 @@ jQuery(function() {
     jQuery('.formtools-element-form').on('submit', formTools.elementSubmit);
     jQuery('.formtools-page-form').on('submit', formTools.pageSubmit);
     jQuery('.formtools-delete-page').on('click', formTools.deletePage);
+    jQuery('.formtools-component-menu input[name=search]').on('propertychange change keyup paste input', formTools.refreshSource);
 });
 </script>
 <%INIT>
diff --git a/static/js/rt-extension-formtools.js b/static/js/rt-extension-formtools.js
index 2e3ef2d..9ab0605 100644
--- a/static/js/rt-extension-formtools.js
+++ b/static/js/rt-extension-formtools.js
@@ -198,5 +198,26 @@ formTools = {
             tab.remove();
         });
         return false;
+    },
+
+    refreshSource: function () {
+        var searchTerm = jQuery(this).val().toLowerCase();
+        if ( searchTerm.length ) {
+            // Hide the separator on search, considering some sections might not have any matched items.
+            jQuery('.formtools-component-menu').find('hr').hide();
+            jQuery('.formtools-component-menu').find('.formtools-element').each(function () {
+                var item = jQuery(this);
+                if (item.find('span.content').text().toLowerCase().indexOf(searchTerm) > -1) {
+                    item.show();
+                }
+                else {
+                    item.hide();
+                }
+            });
+        }
+        else {
+            jQuery('.formtools-component-menu').find('hr').show();
+            jQuery('.formtools-component-menu').find('.formtools-element').show();
+        }
     }
 };

commit 3efc7e92be58e8eb186cea0c1be90ec4a1bf20b9
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Tue Oct 31 14:33:17 2023 -0400

    Wrap name to span.content for core and custom fields
    
    This is consistent with HTML items, and then we can filter the source
    list with span.content text.

diff --git a/html/Admin/FormTools/Modify.html b/html/Admin/FormTools/Modify.html
index 5150912..ea53511 100644
--- a/html/Admin/FormTools/Modify.html
+++ b/html/Admin/FormTools/Modify.html
@@ -60,7 +60,7 @@
           <p class="formtools-element-placeholder m-1 p-2 border rounded"><% loc('Place here') %></p>
           <p class="m-1 p-2 border rounded">
             <span class="badge badge-primary"><% loc('Custom Field') %></span>
-            <% $item %> <span class="label"></span>
+            <span class="content"><% $item %> <span class="label"></span></span>
             <a href="#" class="edit" data-toggle="modal" data-target="#formtools-element-modal">
               <span class="fas fa-pencil-alt" alt="<% loc('Edit') %>" data-toggle="tooltip" data-placement="top" data-original-title="<% loc('Edit') %>"></span>
             </a>
@@ -104,7 +104,7 @@
           <p class="formtools-element-placeholder m-1 p-2 border rounded"><% loc('Place here') %></p>
           <p class="m-1 p-2 border rounded">
             <span class="badge badge-primary"><% loc('Custom Field') %></span>
-            <% $item %> <span class="label"></span>
+            <span class="content"><% $item %> <span class="label"></span></span>
             <a href="#" class="edit" data-toggle="modal" data-target="#formtools-element-modal">
               <span class="fas fa-pencil-alt" alt="<% loc('Edit') %>" data-toggle="tooltip" data-placement="top" data-original-title="<% loc('Edit') %>"></span>
             </a>

commit 9086ba9d552dd8742b318a072993194e0c2b15ae
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Tue Oct 31 10:52:30 2023 -0400

    Sync page name and tab text on name update

diff --git a/html/Admin/FormTools/Modify.html b/html/Admin/FormTools/Modify.html
index a52db1e..5150912 100644
--- a/html/Admin/FormTools/Modify.html
+++ b/html/Admin/FormTools/Modify.html
@@ -221,10 +221,10 @@
     <div class="tab-content">
 %   foreach my $page_name (@form_pages) {
       <div id="formtools-content-<% $form_page_id{$page_name} %>" class="tab-pane <% $form_page_id{$page_name} eq $active_context->{tab} ? 'show active' : 'fade' %>" role="tabpanel">
-        <div class="modal fade formtools-element-modal" id="formtools-element-<% $form_page_id{$page_name} %>-basics-modal" tabindex="-1" role="dialog">
+        <div class="modal fade formtools-page-modal" id="formtools-element-<% $form_page_id{$page_name} %>-basics-modal" tabindex="-1" role="dialog">
           <div class="modal-dialog" role="document">
             <div class="modal-content">
-              <form class="formtools-element-form">
+              <form class="formtools-page-form">
                 <div class="modal-header">
                   <h5 class="modal-title"><% loc('Modify Basics') %></h5>
                   <a href="javascript:void(0)" class="close" data-dismiss="modal" aria-label="Close">
@@ -399,6 +399,7 @@
 jQuery(function() {
     jQuery('#formtools-form-modify').on('submit', formTools.submit);
     jQuery('.formtools-element-form').on('submit', formTools.elementSubmit);
+    jQuery('.formtools-page-form').on('submit', formTools.pageSubmit);
     jQuery('.formtools-delete-page').on('click', formTools.deletePage);
 });
 </script>
diff --git a/static/js/rt-extension-formtools.js b/static/js/rt-extension-formtools.js
index a10d523..2e3ef2d 100644
--- a/static/js/rt-extension-formtools.js
+++ b/static/js/rt-extension-formtools.js
@@ -146,6 +146,13 @@ formTools = {
         form.closest('.formtools-element-modal').modal('hide');
     },
 
+    pageSubmit: function(e) {
+        e.preventDefault();
+        const form = jQuery(this);
+        jQuery('#formtools-pages a.nav-link.active').text(form.find('input[name=name]').val());
+        form.closest('.formtools-page-modal').modal('hide');
+    },
+
     submit: function(e) {
         const form = jQuery(this);
         const content = {};

commit 9087bcd6fd5c67525efea696575ca2c25afe47b1
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Tue Oct 31 10:51:32 2023 -0400

    Fix page form html: remove duplicate form tag and make "Update" button submit

diff --git a/html/Admin/FormTools/Modify.html b/html/Admin/FormTools/Modify.html
index 392efdb..a52db1e 100644
--- a/html/Admin/FormTools/Modify.html
+++ b/html/Admin/FormTools/Modify.html
@@ -232,32 +232,30 @@
                   </a>
                 </div>
                 <div class="modal-body">
-                  <form>
-                    <&| /Elements/LabeledValue, Label => loc('Page Name') &>
-                      <input name="name" class="form-control" value="<% $form->{'formtools-pages'}{$page_name}{name} %>" />
-                    </&>
-                    <&| /Elements/LabeledValue, Label => loc('Sort Order') &>
-                      <input name="sort_order" class="form-control" value="<% $form->{'formtools-pages'}{$page_name}{sort_order} %>" />
-                    </&>
-                    <&| /Elements/LabeledValue, Label => '' &>
-                      <div class="custom-control custom-checkbox">
-                        <input class="custom-control-input" id="<% $form_page_id{$page_name} %>-validation" type="checkbox" name="validation" value="1" <% $form->{'formtools-pages'}{$page_name}{validation} ? 'checked="checked"' : '' |n%> />
-                        <label class="custom-control-label" for="<% $form_page_id{$page_name} %>-validation">
-                          <&|/l&>Enable validation</&>
-                        </label>
-                      </div>
-                    </&>
+                  <&| /Elements/LabeledValue, Label => loc('Page Name') &>
+                    <input name="name" class="form-control" value="<% $form->{'formtools-pages'}{$page_name}{name} %>" />
+                  </&>
+                  <&| /Elements/LabeledValue, Label => loc('Sort Order') &>
+                    <input name="sort_order" class="form-control" value="<% $form->{'formtools-pages'}{$page_name}{sort_order} %>" />
+                  </&>
+                  <&| /Elements/LabeledValue, Label => '' &>
+                    <div class="custom-control custom-checkbox">
+                      <input class="custom-control-input" id="<% $form_page_id{$page_name} %>-validation" type="checkbox" name="validation" value="1" <% $form->{'formtools-pages'}{$page_name}{validation} ? 'checked="checked"' : '' |n%> />
+                      <label class="custom-control-label" for="<% $form_page_id{$page_name} %>-validation">
+                        <&|/l&>Enable validation</&>
+                      </label>
+                    </div>
+                  </&>
 
 %                   # Do not delete the last one
 %                   if ( $form->{'formtools-pages'}{$page_name}{next} ) {
-                    <&| /Elements/LabeledValue, Label => '' &>
-                      <a class="formtools-delete-page btn btn-primary button" data-page="<% $page_name %>" href="#"><% loc('Delete Page') %></a>
-                    </&>
+                  <&| /Elements/LabeledValue, Label => '' &>
+                    <a class="formtools-delete-page btn btn-primary button" data-page="<% $page_name %>" href="#"><% loc('Delete Page') %></a>
+                  </&>
 %                   }
-                  </form>
                 </div>
                 <div class="modal-footer">
-                  <button type="submit" class="btn btn-primary button form-control" data-dismiss="modal"><% loc('Update') %></button>
+                  <button type="submit" class="btn btn-primary button form-control"><% loc('Update') %></button>
                 </div>
               </form>
             </div>

commit 2375ad6e6d72ec1704381b12ad4bf6825c21936c
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Tue Oct 31 10:02:03 2023 -0400

    Put Select/Create under the same page menu like other pages

diff --git a/html/Callbacks/FormTools/Elements/Tabs/Privileged b/html/Callbacks/FormTools/Elements/Tabs/Privileged
index 0445206..43de052 100644
--- a/html/Callbacks/FormTools/Elements/Tabs/Privileged
+++ b/html/Callbacks/FormTools/Elements/Tabs/Privileged
@@ -14,8 +14,14 @@ if ( Menu->child('admin')
 
 if ( $m->request_path =~ m{^/Admin/FormTools/} ) {
     my $page = PageMenu();
-    $page->child( select => title => loc('Select'), path => "/Admin/FormTools/index.html" );
-    $page->child( create => title => loc('Create'), path => "/Admin/FormTools/Create.html" );
+    if ( $HTML::Mason::Commands::m->request_args->{'id'} && $HTML::Mason::Commands::m->request_args->{'id'} =~ /^\d+$/ ) {
+        my $forms = $page->child( forms => title => loc('Form Tools'), path => "/Admin/FormTools/" );
+        $forms->child( select => title => loc('Select'), path => "/Admin/FormTools/index.html" );
+        $forms->child( create => title => loc('Create'), path => "/Admin/FormTools/Create.html" );
+    } else {
+        $page->child( select => title => loc('Select'), path => "/Admin/FormTools/index.html" );
+        $page->child( create => title => loc('Create'), path => "/Admin/FormTools/Create.html" );
+    }
 
     if ( ( $HTML::Mason::Commands::DECODED_ARGS->{'id'} || '' ) =~ /^(\d+)$/ ) {
         my $id  = $1;

commit 02b6bad382915393f208bff115ea708bde799399
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Tue Oct 31 09:50:10 2023 -0400

    Make placeholders lighter so it's clear that they are placeholders

diff --git a/static/css/rt-extension-formtools.css b/static/css/rt-extension-formtools.css
index 4e6f8ff..6c42b76 100644
--- a/static/css/rt-extension-formtools.css
+++ b/static/css/rt-extension-formtools.css
@@ -76,3 +76,7 @@ div .formtools-admin-description {
 #formtools-form-body p {
     text-align: center;
 }
+
+.formtools-form-pages input::placeholder {
+    opacity: 0.5;
+}

commit b98d5ce91e175ec6393b429cff2b2ff775917afe
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Tue Oct 31 09:28:07 2023 -0400

    Automatically validate content to indicate errors in real time

diff --git a/html/Admin/FormTools/Advanced.html b/html/Admin/FormTools/Advanced.html
index a6e520f..8a64fb6 100644
--- a/html/Admin/FormTools/Advanced.html
+++ b/html/Admin/FormTools/Advanced.html
@@ -11,6 +11,12 @@
     <textarea class="form-control" rows="30" name="Content"><% $ARGS{Content} || $encoded_content %></textarea>
   </&>
 
+  <div class="form-row invalid-json hidden">
+    <div class="offset-3 col-9">
+      <div class="alert alert-danger mb-0"><&|/l&>Invalid JSON</&></div>
+    </div>
+  </div>
+
   <div class="form-row">
     <div class="col-12">
       <& /Elements/Submit, Label => loc('Save Changes'), Name => 'Update' &>
@@ -19,6 +25,28 @@
 
 </form>
 
+<script type="text/javascript">
+jQuery('form[name=ModifyFormToolsAdvanced] :input[name=Content]').bind('input propertychange', function() {
+    var form = jQuery(this).closest('form');
+    var validate_json = function (str) {
+        try {
+            JSON.parse(str);
+        } catch (e) {
+            return false;
+        }
+        return true;
+    };
+    if ( validate_json(jQuery(this).val()) ) {
+        form.find('input[type=submit]').prop('disabled', false);
+        form.find('.invalid-json').addClass('hidden');
+    }
+    else {
+        form.find('input[type=submit]').prop('disabled', true);
+        form.find('.invalid-json').removeClass('hidden');
+    }
+});
+</script>
+
 <%INIT>
 Abort("No form id found") unless $id;
 

commit f802b0e5ba09eb9731aeed5bac3f013268f1c740
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Mon Oct 30 18:09:06 2023 -0400

    Fix HTML ids for cf elements where it might contain special chars like spaces

diff --git a/html/Admin/FormTools/Modify.html b/html/Admin/FormTools/Modify.html
index d660fb0..392efdb 100644
--- a/html/Admin/FormTools/Modify.html
+++ b/html/Admin/FormTools/Modify.html
@@ -100,7 +100,7 @@
 % }
         <hr />
 % foreach my $item ( @custom_fields ) {
-        <div id="formtools-element-<% $item %>" class="formtools-element" draggable="true" ondragstart="formTools.dragstart(event);" ondragend="formTools.dragend(event);" data-value="<% JSON({ type => 'component', comp_name => 'Field', arguments => { name => $item } }) %>">
+        <div id="formtools-element-<% CSSClass($item) %>" class="formtools-element" draggable="true" ondragstart="formTools.dragstart(event);" ondragend="formTools.dragend(event);" data-value="<% JSON({ type => 'component', comp_name => 'Field', arguments => { name => $item } }) %>">
           <p class="formtools-element-placeholder m-1 p-2 border rounded"><% loc('Place here') %></p>
           <p class="m-1 p-2 border rounded">
             <span class="badge badge-primary"><% loc('Custom Field') %></span>
@@ -113,7 +113,7 @@
             </a>
           </p>
         </div>
-        <div class="modal fade formtools-element-modal" id="formtools-element-<% $item %>-modal" tabindex="-1" role="dialog">
+        <div class="modal fade formtools-element-modal" id="formtools-element-<% CSSClass($item) %>-modal" tabindex="-1" role="dialog">
           <div class="modal-dialog" role="document">
             <div class="modal-content">
               <form class="formtools-element-form">

commit 0a7c228334241f9e16cdbcffaa556fc9786eb632
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Mon Oct 30 18:03:42 2023 -0400

    Show informative custom labels on element list on Modify page

diff --git a/html/Admin/FormTools/Modify.html b/html/Admin/FormTools/Modify.html
index c83bff4..d660fb0 100644
--- a/html/Admin/FormTools/Modify.html
+++ b/html/Admin/FormTools/Modify.html
@@ -60,7 +60,7 @@
           <p class="formtools-element-placeholder m-1 p-2 border rounded"><% loc('Place here') %></p>
           <p class="m-1 p-2 border rounded">
             <span class="badge badge-primary"><% loc('Custom Field') %></span>
-            <% $item %>
+            <% $item %> <span class="label"></span>
             <a href="#" class="edit" data-toggle="modal" data-target="#formtools-element-modal">
               <span class="fas fa-pencil-alt" alt="<% loc('Edit') %>" data-toggle="tooltip" data-placement="top" data-original-title="<% loc('Edit') %>"></span>
             </a>
@@ -104,7 +104,7 @@
           <p class="formtools-element-placeholder m-1 p-2 border rounded"><% loc('Place here') %></p>
           <p class="m-1 p-2 border rounded">
             <span class="badge badge-primary"><% loc('Custom Field') %></span>
-            <% $item %>
+            <% $item %> <span class="label"></span>
             <a href="#" class="edit" data-toggle="modal" data-target="#formtools-element-modal">
               <span class="fas fa-pencil-alt" alt="<% loc('Edit') %>" data-toggle="tooltip" data-placement="top" data-original-title="<% loc('Edit') %>"></span>
             </a>
@@ -292,8 +292,11 @@
 %                 }
 %               } elsif ( $item->{type} eq 'hidden' ) {
                   <% $item->{'input-name'} %>: <% $item->{'input-value'} %>
-%               } else {
+%               } elsif ( $item->{type} eq 'component' && $item->{comp_name} eq 'Field' ) {
                   <% $item->{arguments}{name} || $item->{comp_name} %>
+                  <span class="label"><% $item->{arguments}{label} ? "($item->{arguments}{label})" : '' %></span>
+%               } else {
+                  <% $item->{comp_name} %>
 %               }
                 </span>
 %             if ( $item->{type} eq 'raw_html' || ( $item->{type} eq 'component' && $item->{comp_name} eq 'Field' ) || $item->{type} eq 'hidden' ) {
diff --git a/static/js/rt-extension-formtools.js b/static/js/rt-extension-formtools.js
index 9a227f0..a10d523 100644
--- a/static/js/rt-extension-formtools.js
+++ b/static/js/rt-extension-formtools.js
@@ -102,8 +102,10 @@ formTools = {
             const label = form.find(':input[name=label]').val();
             if ( label.length ) {
                 value.arguments.label = label;
+                element.find('span.label').text('(' + label + ')');
             }
             else {
+                element.find('span.label').text('');
                 delete value.arguments.label;
             }
 

commit 6b6a77de305e36d28065b7fb112d8c02f6b87b92
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Mon Oct 30 17:29:14 2023 -0400

    Change submit buttons in modals from "Save" to "Update"
    
    It doesn't actually save from the modal, but the "Save" button implies
    that it does.

diff --git a/html/Admin/FormTools/Modify.html b/html/Admin/FormTools/Modify.html
index 2b98c37..c83bff4 100644
--- a/html/Admin/FormTools/Modify.html
+++ b/html/Admin/FormTools/Modify.html
@@ -46,7 +46,7 @@
                   </&>
                 </div>
                 <div class="modal-footer">
-                  <button type="submit" class="btn btn-primary button form-control"><% loc('Save') %></button>
+                  <button type="submit" class="btn btn-primary button form-control"><% loc('Update') %></button>
                 </div>
               </form>
             </div>
@@ -91,7 +91,7 @@
                   </&>
                 </div>
                 <div class="modal-footer">
-                  <button type="submit" class="btn btn-primary button form-control"><% loc('Save') %></button>
+                  <button type="submit" class="btn btn-primary button form-control"><% loc('Update') %></button>
                 </div>
               </form>
             </div>
@@ -143,7 +143,7 @@
                   </&>
                 </div>
                 <div class="modal-footer">
-                  <button type="submit" class="btn btn-primary button form-control"><% loc('Save') %></button>
+                  <button type="submit" class="btn btn-primary button form-control"><% loc('Update') %></button>
                 </div>
               </form>
             </div>
@@ -188,7 +188,7 @@
                   </&>
                 </div>
                 <div class="modal-footer">
-                  <button type="submit" class="btn btn-primary button form-control"><% loc('Save') %></button>
+                  <button type="submit" class="btn btn-primary button form-control"><% loc('Update') %></button>
                 </div>
               </form>
             </div>
@@ -257,7 +257,7 @@
                   </form>
                 </div>
                 <div class="modal-footer">
-                  <button type="submit" class="btn btn-primary button form-control" data-dismiss="modal"><% loc('Save') %></button>
+                  <button type="submit" class="btn btn-primary button form-control" data-dismiss="modal"><% loc('Update') %></button>
                 </div>
               </form>
             </div>
@@ -365,7 +365,7 @@
 %                   }
                     </div>
                     <div class="modal-footer">
-                      <button type="submit" class="btn btn-primary button form-control"><% loc('Save') %></button>
+                      <button type="submit" class="btn btn-primary button form-control"><% loc('Update') %></button>
                     </div>
                   </form>
                 </div>

commit 26ce234391a9b4bb90c9005946c9a51ca2ca29f2
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Mon Oct 30 17:24:26 2023 -0400

    Fix edit links for items just added
    
    The links are not in modals.

diff --git a/static/js/rt-extension-formtools.js b/static/js/rt-extension-formtools.js
index a177839..9a227f0 100644
--- a/static/js/rt-extension-formtools.js
+++ b/static/js/rt-extension-formtools.js
@@ -54,6 +54,7 @@ formTools = {
             const old_id = source_copy.id;
             source_copy.id = 'formtools-element-' + area.dataset.pageId + '-' + Date.now();
             jQuery(source_copy).attr('ondragenter', 'formTools.dragenter(event);');
+            jQuery(source_copy).find('a.edit').attr('data-target', '#' + source_copy.id + '-modal' );
             if ( sibling ) {
                 area.insertBefore(source_copy, sibling);
             }
@@ -64,7 +65,6 @@ formTools = {
             const modal_copy = jQuery('#' + old_id + '-modal').clone(true);
             jQuery('div.modal-wrapper:visible').append(modal_copy);
             modal_copy.attr('id', source_copy.id + '-modal' );
-            modal_copy.find('a.edit').attr('data-target', '#' + source_copy.id + '-modal' );
             modal_copy.find('form.formtools-element-form').on('submit', formTools.elementSubmit);
             modal_copy.modal('show');
             modal_copy.attr('ondragenter', 'formTools.dragenter(event);');

commit 6fd892e0adc4eeb4b2d1649e1a4957ad9ea24614
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Mon Oct 30 16:53:23 2023 -0400

    Center p tags on form pages
    
    It looks strange for left justified p tags with a short bit of text.

diff --git a/static/css/rt-extension-formtools.css b/static/css/rt-extension-formtools.css
index 57e41b1..4e6f8ff 100644
--- a/static/css/rt-extension-formtools.css
+++ b/static/css/rt-extension-formtools.css
@@ -72,3 +72,7 @@ div .formtools-admin-description {
 #formtools-edit .card-body {
     padding: 15px 15px 15px 15px;
 }
+
+#formtools-form-body p {
+    text-align: center;
+}

commit 1cc6f84783cc8a25d7b2a14d6eeeee02db2a8fd0
Author: Jason Crome <jcrome at bestpractical.com>
Date:   Mon Oct 30 16:16:52 2023 -0400

    RT-Extension-FormTools 0.54

diff --git a/Changes b/Changes
index 6a4ffcd..e0ab4af 100644
--- a/Changes
+++ b/Changes
@@ -1,5 +1,8 @@
 Revision history for RT-Extension-FormTools
 
+0.54 2023-10-30
+ - Create a graphical editor for forms
+
 0.53 2022-05-03
 
  - Update example to render radio using RT core feature
diff --git a/MANIFEST b/MANIFEST
index cf4c9aa..ae8438d 100644
--- a/MANIFEST
+++ b/MANIFEST
@@ -1,4 +1,6 @@
+bin/rt-insert-formtools-config.in
 Changes
+etc/sample_form.json
 ex/RT-Example-FormTools/etc/initialdata
 ex/RT-Example-FormTools/html/Example/autohandler
 ex/RT-Example-FormTools/html/Example/confirm
@@ -7,12 +9,30 @@ ex/RT-Example-FormTools/html/Example/submit
 ex/RT-Example-FormTools/html/Example/user_info
 ex/RT-Example-FormTools/lib/RT/Example/FormTools.pm
 ex/RT-Example-FormTools/Makefile.PL
+html/Admin/Elements/FormToolsHelp
+html/Admin/FormTools/Advanced.html
+html/Admin/FormTools/Create.html
+html/Admin/FormTools/Describe.html
+html/Admin/FormTools/GroupRights.html
+html/Admin/FormTools/index.html
+html/Admin/FormTools/Modify.html
+html/Admin/FormTools/UserRights.html
 html/Callbacks/FormTools/Elements/Header/Head
+html/Callbacks/FormTools/Elements/Tabs/Privileged
+html/Callbacks/FormTools/Elements/Tabs/SelfService
+html/Elements/Forms/ListForms
+html/Elements/Forms/ShowFormGraphic
+html/Elements/Forms/ShowFormIcon
+html/Forms.html
+html/Forms/dhandler
 html/FormTools/Field
 html/FormTools/Form
 html/FormTools/Next
 html/FormTools/ShowChoices
+html/NoAuth/Helpers/FormLogo/dhandler
 html/NoAuth/js/form_tools.js
+html/SelfService/Forms.html
+html/SelfService/Forms/dhandler
 inc/Module/Install.pm
 inc/Module/Install/Base.pm
 inc/Module/Install/Can.pm
@@ -23,11 +43,16 @@ inc/Module/Install/Metadata.pm
 inc/Module/Install/ReadmeFromPod.pm
 inc/Module/Install/RTx.pm
 inc/Module/Install/RTx/Runtime.pm
+inc/Module/Install/Substitute.pm
 inc/Module/Install/Win32.pm
 inc/Module/Install/WriteAll.pm
 inc/YAML/Tiny.pm
 lib/RT/Extension/FormTools.pm
+lib/RT/Interface/Web_Vendor.pm
 Makefile.PL
 MANIFEST			This list of files
 META.yml
+patches/Support-custom-labels-for-ValidateCustomFields.patch
 README
+static/css/rt-extension-formtools.css
+static/js/rt-extension-formtools.js
diff --git a/META.yml b/META.yml
index 9f86295..5a9fcf9 100644
--- a/META.yml
+++ b/META.yml
@@ -25,6 +25,6 @@ requires:
 resources:
   license: http://opensource.org/licenses/gpl-license.php
   repository: https://github.com/bestpractical/rt-extension-formtools
-version: '0.53'
+version: '0.54'
 x_module_install_rtx_version: '0.43'
 x_requires_rt: 5.0.0
diff --git a/lib/RT/Extension/FormTools.pm b/lib/RT/Extension/FormTools.pm
index 4010f68..2af7c2a 100644
--- a/lib/RT/Extension/FormTools.pm
+++ b/lib/RT/Extension/FormTools.pm
@@ -3,7 +3,7 @@ use strict;
 
 package RT::Extension::FormTools;
 
-our $VERSION = '0.53';
+our $VERSION = '0.54';
 
 RT->AddStyleSheets('rt-extension-formtools.css');
 RT->AddJavaScript('rt-extension-formtools.js');

commit 1f65090fde5b01ca682b7842248ba67d2e38b2dd
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Mon Oct 30 15:49:17 2023 -0400

    Add RT patch to support custom labels in cf validation messages

diff --git a/patches/Support-custom-labels-for-ValidateCustomFields.patch b/patches/Support-custom-labels-for-ValidateCustomFields.patch
new file mode 100644
index 0000000..6a8a2bb
--- /dev/null
+++ b/patches/Support-custom-labels-for-ValidateCustomFields.patch
@@ -0,0 +1,63 @@
+From 33cdfe85441bd338d95eeb7a5d30a293e5e22146 Mon Sep 17 00:00:00 2001
+From: sunnavy <sunnavy at bestpractical.com>
+Date: Mon, 30 Oct 2023 10:56:28 -0400
+Subject: [PATCH] Support custom labels for ValidateCustomFields
+
+This is initially for FormTools, where we support to customize field
+labels, so the validation messages can be consistent with input labels.
+---
+ share/html/Elements/ValidateCustomFields | 10 ++++++----
+ 1 file changed, 6 insertions(+), 4 deletions(-)
+
+diff --git a/share/html/Elements/ValidateCustomFields b/share/html/Elements/ValidateCustomFields
+index 9655db7ce8..df7c752363 100644
+--- a/share/html/Elements/ValidateCustomFields
++++ b/share/html/Elements/ValidateCustomFields
+@@ -82,9 +82,10 @@ while ( my $CF = $CustomFields->Next ) {
+ 
+     $m->notes(('Field-' . $CF->Id) => $submitted->{Values} // $submitted->{Value});
+ 
++    my $label = $Labels{$CF->Name} || $CF->Name;
+     if ( $invalid ) {
+         $m->notes( 'InvalidField-' . $CF->Id => $invalid_message );
+-        push @res, $CF->Name . ': ' . $invalid_message;
++        push @res, $label . ': ' . $invalid_message;
+         $valid = 0;
+         next;
+     }
+@@ -112,7 +113,7 @@ while ( my $CF = $CustomFields->Next ) {
+             my ($ok, $msg) = $CF->_CanonicalizeValue( $ref );
+             unless ($ok) {
+                 $m->notes( ( 'InvalidField-' . $CF->Id ) => $msg );
+-                push @res, $CF->Name .': '. $msg;
++                push @res, $label . ': ' . $msg;
+                 $valid = 0;
+             }
+         }
+@@ -120,7 +121,7 @@ while ( my $CF = $CustomFields->Next ) {
+         if (!$CF->MatchPattern($value)) {
+             my $msg = $CF->FriendlyPattern;
+             $m->notes( ('InvalidField-' . $CF->Id) => $msg );
+-            push @res, $CF->Name .': '. $msg;
++            push @res, $label . ': ' . $msg;
+             $valid = 0;
+         }
+ 
+@@ -138,7 +139,7 @@ while ( my $CF = $CustomFields->Next ) {
+             while (my $ocfv = $existing->Next) {
+                 my $msg = loc("'[_1]' is not a unique value", $value);
+                 $m->notes( ('InvalidField-' . $CF->Id) => $msg );
+-                push @res, $CF->Name .': '. $msg;
++                push @res, $label . ': ' . $msg;
+                 $valid = 0;
+                 last;
+             }
+@@ -153,4 +154,5 @@ $Object => RT::Ticket->new( $session{'CurrentUser'})
+ $CustomFields
+ $ARGSRef
+ $ValidateUnsubmitted => 0
++%Labels => ()
+ </%ARGS>
+-- 
+2.39.3 (Apple Git-145)
+

commit 2d612f085fdddf35c84ce2d1bd9fdd9061443c8d
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Mon Oct 30 15:44:01 2023 -0400

    Pass custom labels to show them in cf validation messages
    
    Previously cf names were shown, which were inconsistent with custom
    labels and could easily confuse people.

diff --git a/html/FormTools/Form b/html/FormTools/Form
index e300468..84b09c2 100644
--- a/html/FormTools/Form
+++ b/html/FormTools/Form
@@ -13,6 +13,7 @@ $form_name           => undef
 $form_classes        => undef
 $self_service        => 0
 $results_ref         => []
+$form_config         => undef
 </%args>
 <%init>
 use RT::Extension::FormTools;
@@ -54,10 +55,15 @@ if ($validation && $real_next) {
         }
     }
 
+    my %labels = map { $_->{arguments}{name} => $_->{arguments}{label} }
+        grep { ( $_->{comp_name} // '' ) eq 'Field' && $_->{arguments}{name} && $_->{arguments}{label} }
+        map { @{ $_->{content} } } values %{ $form_config->{'formtools-pages'} };
+
     my ($status, @msg) = $m->comp(
         '/Elements/ValidateCustomFields',
         CustomFields    => $queue->TicketCustomFields,
         ARGSRef         => \%request_args,
+        Labels          => \%labels,
     );
     unless ($status) {
         push @results, @msg;
diff --git a/html/Forms/dhandler b/html/Forms/dhandler
index 0e84256..6cb11bc 100644
--- a/html/Forms/dhandler
+++ b/html/Forms/dhandler
@@ -4,6 +4,7 @@
     results_ref => \@results,
     self_service => $SelfService,
     include_tabs => 1,
+    form_config => $form_config,
 &>
 
 <%perl>

commit bf65881a63c2da86eed2bded28411a9a17ce21ab
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Fri Oct 27 17:17:46 2023 -0400

    Not only wrap help tooltip in labels
    
    This is the same fix as in RT core.

diff --git a/html/FormTools/Field b/html/FormTools/Field
index f7694be..42fe477 100644
--- a/html/FormTools/Field
+++ b/html/FormTools/Field
@@ -127,7 +127,7 @@ $default = '' unless defined $default;
 
 % if ($render_as ne 'hidden' && $show_label) { # no label if hidden
 
-<&| /Elements/LabeledValue, RawLabel => $m->interp->apply_escapes($field_label, 'h') . $after_label, LabelTooltip => $tooltip &>
+<&| /Elements/LabeledValue, RawLabel => $m->interp->apply_escapes($field_label, 'h') . $after_label, LabelSpanClass => $tooltip ? 'prev-icon-helper' : '', LabelTooltip => $tooltip &>
   <& SELF:Value, %ARGS, input_name => $input_name, field_type => $field_type, default => $default, values => \@values, cf => $cf &>
   <% $after_input |n %>
 </&>

commit 827f574bd6d31cd0717bab6bfdd8916bfc7eb97b
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Fri Oct 27 16:44:54 2023 -0400

    Remove empty cf values to avoid possible validation errors on back
    
    This is to avoid unnecessary cf validations for the following case: user
    clicks "Next" to go to the next page that has a mandatory cf input, then
    clicks "Back" without setting the cf to go to the previous page, then
    clicks "Next" again which caused validation error.

diff --git a/html/Forms/dhandler b/html/Forms/dhandler
index ddf363c..0e84256 100644
--- a/html/Forms/dhandler
+++ b/html/Forms/dhandler
@@ -142,6 +142,12 @@ if ( $ARGS{Back} and $ARGS{Back} eq 'Back' ) {
     $_form_tools_next = '';
     delete $ARGS{'_form_tools_next'} if $ARGS{'_form_tools_next'};
 
+    for my $field ( grep { /Object-RT::Ticket--CustomField-/ } keys %ARGS ) {
+        next if length $ARGS{$field};
+        delete $ARGS{$field};
+        delete $ARGS{"$field-Magic"};
+    }
+
     # Redirect to the previous page
     MaybeRedirectForResults(
         Path      => $base_path . $form_name . '/' . $back_page,

commit 075699f085165988f1316f1cce14704ae155acef
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Fri Oct 27 16:26:07 2023 -0400

    Make rt-insert-formtools-config executable

diff --git a/bin/rt-insert-formtools-config.in b/bin/rt-insert-formtools-config.in
old mode 100644
new mode 100755

commit 5de9b38f31ce3e23e80976bd6895f8ef62d4d623
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Fri Oct 27 15:15:43 2023 -0400

    Add ShowForm right to control the visibility of forms

diff --git a/html/Admin/FormTools/GroupRights.html b/html/Admin/FormTools/GroupRights.html
new file mode 100644
index 0000000..791ffd0
--- /dev/null
+++ b/html/Admin/FormTools/GroupRights.html
@@ -0,0 +1,37 @@
+<& /Admin/Elements/Header, Title => $title &>
+<& /Elements/Tabs &>
+<& /Elements/ListActions, actions => \@results &>
+
+<form method="post" action="GroupRights.html" id="ModifyGroupRights" name="ModifyGroupRights">
+  <input type="hidden" class="hidden" name="id" value="<% $form_attribute->id %>" />
+
+  <& /Admin/Elements/EditRights, Context => $form_attribute, Principals => \@principals &>
+  <div class="form-row">
+    <div class="col-12">
+      <& /Elements/Submit, Label => loc('Save Changes') &>
+    </div>
+  </div>
+</form>
+
+<%INIT>
+
+Abort("No form id found") unless $id;
+
+my $form_attribute = RT::Attribute->new($session{'CurrentUser'});
+my ($ok, $msg) = $form_attribute->Load($id);
+
+unless ( $ok ) {
+    Abort("Unable to load form with id $id");
+}
+
+my @results = ProcessACLs( \%ARGS );
+
+my $title = loc('Modify group rights for Form [_1]', $form_attribute->Description);
+
+# Principal collections
+my @principals = GetPrincipalsMap($form_attribute, qw(System Groups));
+</%INIT>
+
+<%ARGS>
+$id => undef
+</%ARGS>
diff --git a/html/Admin/FormTools/UserRights.html b/html/Admin/FormTools/UserRights.html
new file mode 100644
index 0000000..9447913
--- /dev/null
+++ b/html/Admin/FormTools/UserRights.html
@@ -0,0 +1,37 @@
+<& /Admin/Elements/Header, Title => $title &>
+<& /Elements/Tabs &>
+<& /Elements/ListActions, actions => \@results &>
+
+<form method="post" action="UserRights.html" id="ModifyUserRights" name="ModifyUserRights">
+  <input type="hidden" class="hidden" name="id" value="<% $form_attribute->id %>" />
+
+  <& /Admin/Elements/EditRights, Context => $form_attribute, Principals => \@principals &>
+  <div class="form-row">
+    <div class="col-12">
+      <& /Elements/Submit, Label => loc('Save Changes') &>
+    </div>
+  </div>
+</form>
+
+<%INIT>
+
+Abort("No form id found") unless $id;
+
+my $form_attribute = RT::Attribute->new($session{'CurrentUser'});
+my ($ok, $msg) = $form_attribute->Load($id);
+
+unless ( $ok ) {
+    Abort("Unable to load form with id $id");
+}
+
+my @results = ProcessACLs( \%ARGS );
+
+my $title = loc('Modify user rights for Form [_1]', $form_attribute->Description);
+
+# Principal collections
+my @principals = GetPrincipalsMap($form_attribute, qw(Users));
+</%INIT>
+
+<%ARGS>
+$id => undef
+</%ARGS>
diff --git a/html/Callbacks/FormTools/Elements/Tabs/Privileged b/html/Callbacks/FormTools/Elements/Tabs/Privileged
index 8074fd6..0445206 100644
--- a/html/Callbacks/FormTools/Elements/Tabs/Privileged
+++ b/html/Callbacks/FormTools/Elements/Tabs/Privileged
@@ -29,6 +29,8 @@ if ( $m->request_path =~ m{^/Admin/FormTools/} ) {
         $page->child( modify => title => loc('Modify'), path => "/Admin/FormTools/Modify.html?id=" . $id );
         $page->child( description => title => loc('Description'), path => "/Admin/FormTools/Describe.html?id=" . $id );
         $page->child( advanced => title => loc('Advanced'), path => "/Admin/FormTools/Advanced.html?id=" . $id );
+        $page->child( 'group-rights' => title => loc('Group Rights'), path => "/Admin/FormTools/GroupRights.html?id=" . $id );
+        $page->child( 'user-rights' => title => loc('User Rights'), path => "/Admin/FormTools/UserRights.html?id=" . $id );
     }
 }
 
diff --git a/html/Elements/Forms/ListForms b/html/Elements/Forms/ListForms
index c7afbfd..6f7b919 100644
--- a/html/Elements/Forms/ListForms
+++ b/html/Elements/Forms/ListForms
@@ -1,6 +1,7 @@
 <&|/Widgets/TitleBox, title => '', class => 'container formtools-form-list ml-auto mr-auto' &>
 % my $item_number = 1;
 % while ( my $form_attribute = $forms->Next ) {
+%    next unless $form_attribute->CurrentUserCanSee;
 %    my $form = $form_attribute->Content;
 %    next if $form->{disabled};
 <div class="row mt-3 mb-3">
@@ -18,7 +19,7 @@
 % }
 </&>
 <%init>
-my $forms = RT::Attributes->new( RT->SystemUser );
+my $forms = RT::Attributes->new( $session{CurrentUser} );
 $forms->Limit( FIELD => 'Name', VALUE => 'FormTools Form' );
 
 my $SelfServicePath = '/SelfService';
diff --git a/html/Forms/dhandler b/html/Forms/dhandler
index 9a73a1d..ddf363c 100644
--- a/html/Forms/dhandler
+++ b/html/Forms/dhandler
@@ -63,11 +63,12 @@ if ( $page_name ) {
 
 # Load FormTools configration and look for a configured
 # form with the provided name.
-my $form_attribute = RT::Attribute->new( RT->SystemUser );
+my $form_attribute = RT::Attribute->new( $session{CurrentUser} );
 $form_attribute->LoadByCols( Name => 'FormTools Form', Description => $form_name );
 my $form_config;
 
 if ( $form_attribute->Id ) {
+    Abort( loc("Permission Denied") ) unless $form_attribute->CurrentUserCanSee;
     $form_config = $form_attribute->Content;
 }
 else {
diff --git a/lib/RT/Extension/FormTools.pm b/lib/RT/Extension/FormTools.pm
index 238ed56..4010f68 100644
--- a/lib/RT/Extension/FormTools.pm
+++ b/lib/RT/Extension/FormTools.pm
@@ -49,6 +49,47 @@ sub _GeneratePageId {
     return substr( sha1_hex( time . int rand 10000 ), 0, 8 );
 }
 
+{
+    package RT::Attribute;
+    no warnings 'redefine';
+    use Role::Basic 'with';
+    with "RT::Record::Role::Rights";
+
+    my $orig_available_rights = RT::Attribute->can('AvailableRights');
+    *AvailableRights = sub {
+        my $self = shift;
+
+        if ( $self->__Value('Name') eq 'FormTools Form' ) {
+            return { ShowForm => 'View forms' };
+        }
+        return $orig_available_rights->($self, @_);
+    };
+
+    my $orig_right_categories = RT::Attribute->can('RightCategories');
+    *RightCategories = sub {
+        my $self = shift;
+
+        if ( $self->__Value('Name') eq 'FormTools Form' ) {
+            return { ShowForm => 'General' };
+        }
+        return $orig_right_categories->($self, @_);
+    };
+
+    my $orig_current_user_has_right = RT::Attribute->can('CurrentUserHasRight');
+    *CurrentUserHasRight = sub {
+        my $self  = shift;
+        my $right = shift;
+        if ( $self->__Value('Name') eq 'FormTools Form' ) {
+            return 1 if $self->CurrentUser->HasRight( Object => RT->System, Right => 'AdminForm' );
+            $right = 'ShowForm' if $right eq 'display';
+            return $self->CurrentUser->HasRight( Object => $self, Right => $right );
+        }
+        return $orig_current_user_has_right->( $self, $right, @_ );
+    };
+
+    RT::Attribute->AddRight( General => ShowForm => 'View forms' ); # loc
+}
+
 =head1 NAME
 
 RT-Extension-FormTools - Help write multi-page ticket creation wizards

commit adda559a8036236d8743c1c86b1d5a84ab144219
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Fri Oct 27 02:18:50 2023 -0400

    Add AdminForm so non-superusers can admin forms too

diff --git a/html/Admin/FormTools/Advanced.html b/html/Admin/FormTools/Advanced.html
index 7605e5f..a6e520f 100644
--- a/html/Admin/FormTools/Advanced.html
+++ b/html/Admin/FormTools/Advanced.html
@@ -37,7 +37,7 @@ $title = loc("Modify form [_1]", $form_attribute->Description);
 
 if ( $Update ) {
     Abort( loc('Permission Denied') )
-        unless $session{'CurrentUser'}->HasRight( Object => $RT::System, Right => 'SuperUser' );
+        unless $session{'CurrentUser'}->HasRight( Object => $RT::System, Right => 'AdminForm' );
 
     if ( $ARGS{Description} ) {
 
diff --git a/html/Admin/FormTools/Create.html b/html/Admin/FormTools/Create.html
index 9c27b61..0ab9ed1 100644
--- a/html/Admin/FormTools/Create.html
+++ b/html/Admin/FormTools/Create.html
@@ -20,7 +20,7 @@
 </&>
 <%INIT>
 Abort( loc('Permission Denied') )
-    unless $session{'CurrentUser'}->HasRight( Object => $RT::System, Right => 'SuperUser' );
+    unless $session{'CurrentUser'}->HasRight( Object => $RT::System, Right => 'AdminForm' );
 
 my ($title, @results);
 $title = loc('Create form');
diff --git a/html/Admin/FormTools/Describe.html b/html/Admin/FormTools/Describe.html
index a4a1635..b921c54 100644
--- a/html/Admin/FormTools/Describe.html
+++ b/html/Admin/FormTools/Describe.html
@@ -60,7 +60,7 @@ $title = loc("Description for form [_1]", $form_attribute->Description);
 
 if ( $ARGS{'SubmitDescription'} ) {
     Abort( loc('Permission Denied') )
-        unless $session{'CurrentUser'}->HasRight( Object => $RT::System, Right => 'SuperUser' );
+        unless $session{'CurrentUser'}->HasRight( Object => $RT::System, Right => 'AdminForm' );
 
     if ( $form->{'form-description'} ne $ARGS{'FormDescription'} ) {
         $form->{'form-description'} = $ARGS{'FormDescription'};
diff --git a/html/Admin/FormTools/Modify.html b/html/Admin/FormTools/Modify.html
index 9d8d284..2b98c37 100644
--- a/html/Admin/FormTools/Modify.html
+++ b/html/Admin/FormTools/Modify.html
@@ -418,7 +418,7 @@ my @results;
 
 if ( $AddPage ) {
     Abort( loc('Permission Denied') )
-        unless $session{'CurrentUser'}->HasRight( Object => $RT::System, Right => 'SuperUser' );
+        unless $session{'CurrentUser'}->HasRight( Object => $RT::System, Right => 'AdminForm' );
 
     my @orders = sort { $a <=> $b } map { $form->{'formtools-pages'}{$_}{sort_order} } keys %{$form->{'formtools-pages'}};
 
@@ -443,7 +443,7 @@ if ( $AddPage ) {
 }
 elsif ( $Update ) {
     Abort( loc('Permission Denied') )
-        unless $session{'CurrentUser'}->HasRight( Object => $RT::System, Right => 'SuperUser' );
+        unless $session{'CurrentUser'}->HasRight( Object => $RT::System, Right => 'AdminForm' );
 
     my $new_content = eval { JSON::from_json( $ARGS{Content} ) };
     if ( $@ ) {
diff --git a/html/Callbacks/FormTools/Elements/Tabs/Privileged b/html/Callbacks/FormTools/Elements/Tabs/Privileged
index 7c48c7b..8074fd6 100644
--- a/html/Callbacks/FormTools/Elements/Tabs/Privileged
+++ b/html/Callbacks/FormTools/Elements/Tabs/Privileged
@@ -1,6 +1,8 @@
 <%init>
 
-if ( $session{'CurrentUser'}->HasRight( Object => RT->System, Right => 'SuperUser' ) ) {
+if ( Menu->child('admin')
+    && $session{'CurrentUser'}->HasRight( Object => RT->System, Right => 'AdminForm' ) )
+{
     my $formtools = Menu->child('admin')->child(
         formtools => title => loc('FormTools'),
         path      => '/Admin/FormTools/',
diff --git a/lib/RT/Extension/FormTools.pm b/lib/RT/Extension/FormTools.pm
index 7a85f9a..238ed56 100644
--- a/lib/RT/Extension/FormTools.pm
+++ b/lib/RT/Extension/FormTools.pm
@@ -8,6 +8,8 @@ our $VERSION = '0.53';
 RT->AddStyleSheets('rt-extension-formtools.css');
 RT->AddJavaScript('rt-extension-formtools.js');
 
+RT::System->AddRight( Admin => AdminForm => 'Create, modify and disable forms' ); # loc
+
 use Time::HiRes 'time';
 use Digest::SHA 'sha1_hex';
 

commit 00680b2e017c07f7727b1c03f2d00ea8f1b60db5
Merge: ff3a00c bb18d65
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Fri Oct 20 16:44:38 2023 -0400

    Merge branch 'revise-admin-labels' into dynamic-forms-from-config


commit ff3a00c825a38343f01f2b83b2561eff1dc91e6f
Merge: a5121cf 2fa490e
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Fri Oct 20 16:25:32 2023 -0400

    Merge branch 'privileged-forms-listing' into dynamic-forms-from-config


commit bb18d652f734805b9738da40bb8b54e88c64d421
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Thu Oct 19 11:57:20 2023 -0400

    Update admin help, labels, spacing

diff --git a/html/Admin/Elements/FormToolsHelp b/html/Admin/Elements/FormToolsHelp
index 2df8c64..46102f2 100644
--- a/html/Admin/Elements/FormToolsHelp
+++ b/html/Admin/Elements/FormToolsHelp
@@ -47,6 +47,8 @@
 %# END BPS TAGGED BLOCK }}}
 <div class="help" id="rt-help-text">
 <&| /Widgets/TitleBox, title => loc('FormTools Configuration') &>
-<p>FormTools help goes here.</p>
+<p>Build your form using the tools below. To create new pages for a multi-page form, click the plus (+). To change the order of pages, click the gear and update the Sort order. To remove a page, click the gear and click Delete. Make sure to Save Changes at the bottom of the form after any updates.<p>
+
+<p>To create your form, drag components from the Components box on the left to the Content area on the right. Click the pencil to edit components. Click the X to remove them. Click Save Changes to save the new page.</p>
 </&>
 </div>
diff --git a/html/Admin/FormTools/Modify.html b/html/Admin/FormTools/Modify.html
index e7e72dc..9d8d284 100644
--- a/html/Admin/FormTools/Modify.html
+++ b/html/Admin/FormTools/Modify.html
@@ -9,7 +9,7 @@
       <div class="d-block text-center">
 % foreach my $item ( @html_components ) {
         <div id="formtools-element-<% $item %>" class="formtools-element" draggable="true" ondragstart="formTools.dragstart(event);" ondragend="formTools.dragend(event);" data-value="<% JSON({ type => 'raw_html', html => "<$item>test</$item>"}) %>">
-          <p class="formtools-element-placeholder m-1 p-2 border rounded"><% loc('Put it here') %></p>
+          <p class="formtools-element-placeholder m-1 p-2 border rounded"><% loc('Place here') %></p>
           <p class="m-1 p-2 border rounded">
             <span class="badge badge-primary"><% $item eq 'html' ? 'HTML' : uc($item) %></span>
             <span class="content"><% loc('[_1] Element', uc($item)) %></span>
@@ -57,9 +57,9 @@
         <hr />
 % foreach my $item ( @core_components ) {
         <div id="formtools-element-<% $item %>" class="formtools-element" draggable="true" ondragstart="formTools.dragstart(event);" ondragend="formTools.dragend(event);" data-value="<% JSON({ type => 'component', comp_name => 'Field', arguments => { name => $item } }) %>">
-          <p class="formtools-element-placeholder m-1 p-2 border rounded"><% loc('Put it here') %></p>
+          <p class="formtools-element-placeholder m-1 p-2 border rounded"><% loc('Place here') %></p>
           <p class="m-1 p-2 border rounded">
-            <span class="badge badge-primary"><% loc('Field') %></span>
+            <span class="badge badge-primary"><% loc('Custom Field') %></span>
             <% $item %>
             <a href="#" class="edit" data-toggle="modal" data-target="#formtools-element-modal">
               <span class="fas fa-pencil-alt" alt="<% loc('Edit') %>" data-toggle="tooltip" data-placement="top" data-original-title="<% loc('Edit') %>"></span>
@@ -101,9 +101,9 @@
         <hr />
 % foreach my $item ( @custom_fields ) {
         <div id="formtools-element-<% $item %>" class="formtools-element" draggable="true" ondragstart="formTools.dragstart(event);" ondragend="formTools.dragend(event);" data-value="<% JSON({ type => 'component', comp_name => 'Field', arguments => { name => $item } }) %>">
-          <p class="formtools-element-placeholder m-1 p-2 border rounded"><% loc('Put it here') %></p>
+          <p class="formtools-element-placeholder m-1 p-2 border rounded"><% loc('Place here') %></p>
           <p class="m-1 p-2 border rounded">
-            <span class="badge badge-primary"><% loc('Field') %></span>
+            <span class="badge badge-primary"><% loc('Custom Field') %></span>
             <% $item %>
             <a href="#" class="edit" data-toggle="modal" data-target="#formtools-element-modal">
               <span class="fas fa-pencil-alt" alt="<% loc('Edit') %>" data-toggle="tooltip" data-placement="top" data-original-title="<% loc('Edit') %>"></span>
@@ -154,7 +154,7 @@
         <hr />
 % foreach my $item ( sort keys %other_components ) {
         <div id="formtools-element-<% $item %>" class="formtools-element" draggable="true" ondragstart="formTools.dragstart(event);" ondragend="formTools.dragend(event);" data-value="<% JSON($other_components{$item}) %>">
-          <p class="formtools-element-placeholder m-1 p-2 border rounded"><% loc('Put it here') %></p>
+          <p class="formtools-element-placeholder m-1 p-2 border rounded"><% loc('Place here') %></p>
           <p class="m-1 p-2 border rounded">
             <span class="badge badge-primary"><% $other_components{$item}{type} eq 'hidden' ? loc('Hidden') : loc('Component') %></span>
             <span class="content"><% $item %></span>
@@ -233,7 +233,7 @@
                 </div>
                 <div class="modal-body">
                   <form>
-                    <&| /Elements/LabeledValue, Label => loc('Name') &>
+                    <&| /Elements/LabeledValue, Label => loc('Page Name') &>
                       <input name="name" class="form-control" value="<% $form->{'formtools-pages'}{$page_name}{name} %>" />
                     </&>
                     <&| /Elements/LabeledValue, Label => loc('Sort Order') &>
@@ -270,7 +270,7 @@
 %         my $i = 0;
 %         for my $item ( grep { $_->{type} ne 'hidden' || $_->{'input-name'} ne 'create_ticket' } @{$form->{'formtools-pages'}{$page_name}{content} || []} ) {
             <div id="formtools-element-<% $form_page_id{$page_name} %>-<% $i %>" class="formtools-element" draggable="true" ondragstart="formTools.dragstart(event);" ondragend="formTools.dragend(event);" ondragenter="formTools.dragenter(event)" ondragleave="formTools.dragleave(event)" data-value="<% JSON($item) %>">
-              <p class="formtools-element-placeholder m-1 p-2 border rounded"><% loc('Put it here') %></p>
+              <p class="formtools-element-placeholder m-1 p-2 border rounded"><% loc('Place here') %></p>
               <p class="m-1 p-2 border rounded">
                 <span class="badge badge-primary">
 %                 if ( $item->{type} eq 'raw_html' ) {
@@ -280,7 +280,7 @@
 %                 } elsif ( $item->{type} eq 'hidden' ) {
                     <% loc('Hidden') %>
 %                 } else {
-                    <% loc('Field') %>
+                    <% loc('Custom Field') %>
 %                 }
                 </span>
                 <span class="content">
@@ -308,7 +308,7 @@
             </div>
 %           $i++;
 %         }
-            <p class="formtools-element-placeholder m-1 p-2 border rounded"><% loc('Put it here') %></p>
+            <p class="formtools-element-placeholder m-1 p-2 border rounded"><% loc('Place here') %></p>
           </div>
           <div class="modal-wrapper">
 %         $i = 0;
diff --git a/static/css/rt-extension-formtools.css b/static/css/rt-extension-formtools.css
index 575b09c..57e41b1 100644
--- a/static/css/rt-extension-formtools.css
+++ b/static/css/rt-extension-formtools.css
@@ -68,3 +68,7 @@ div .formtools-admin-description {
     color: black;
     line-height: 1.5;
 }
+
+#formtools-edit .card-body {
+    padding: 15px 15px 15px 15px;
+}

commit ff0dc2de3b83abd9f78a9151ec832374161af623
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Thu Oct 19 11:56:06 2023 -0400

    Update results content to new page data structure

diff --git a/html/Admin/FormTools/Modify.html b/html/Admin/FormTools/Modify.html
index e53d21e..e7e72dc 100644
--- a/html/Admin/FormTools/Modify.html
+++ b/html/Admin/FormTools/Modify.html
@@ -531,12 +531,16 @@ $form->{'formtools-pages'} ||= {
         name       => 'Result',
         content    => [
             {
-                type => 'raw_html',
-                html => '<h2>Request Submitted</h2>',
+               "content" => "Request Submitted",
+               "html"    => "<h2>Request Submitted</h2>",
+               "type"    => "raw_html",
+               "wrapper" => "h2"
             },
             {
-                type => 'raw_html',
-                html => '<p>Your request has been submitted.</p>',
+               "content" => "Your request has been submitted.",
+               "html"    => "<p>Your request has been submitted.</p>",
+               "type"    => "raw_html",
+               "wrapper" => "p"
             },
         ],
     },

commit 88aff705bd26edabb8abbc968a54429c33f91fc0
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Wed Oct 18 16:05:54 2023 -0400

    Add titleboxes to additional form pages

diff --git a/html/Admin/FormTools/Create.html b/html/Admin/FormTools/Create.html
index 58f2ed1..9c27b61 100644
--- a/html/Admin/FormTools/Create.html
+++ b/html/Admin/FormTools/Create.html
@@ -2,6 +2,7 @@
 <& /Elements/Tabs &>
 <& /Elements/ListActions, actions => \@results &>
 
+<&| /Widgets/TitleBox, title => '', class => 'container w-75' &>
 <form action="<%RT->Config->Get('WebPath')%>/Admin/FormTools/Create.html" name="CreateFormTools" method="post" enctype="multipart/form-data" class="mx-auto max-width-sm">
   <&| /Elements/LabeledValue, Label => loc('Name') &>
     <input name="Description" class="form-control" value="<% $ARGS{Description} // '' %>"/>
@@ -16,7 +17,7 @@
   </div>
 
 </form>
-
+</&>
 <%INIT>
 Abort( loc('Permission Denied') )
     unless $session{'CurrentUser'}->HasRight( Object => $RT::System, Right => 'SuperUser' );
diff --git a/html/Admin/FormTools/index.html b/html/Admin/FormTools/index.html
index 3c00092..219a407 100644
--- a/html/Admin/FormTools/index.html
+++ b/html/Admin/FormTools/index.html
@@ -2,18 +2,8 @@
 <& /Elements/Tabs &>
 <& /Elements/ListActions, actions => \@results &>
 
+<&| /Widgets/TitleBox, title => '', class => 'container formtools-admin-select' &>
 <form method="post" action="<% RT->Config->Get('WebPath') %>/Admin/FormTools/index.html">
-  <div class="form-row">
-    <div class="col-auto">
-      <div class="custom-control custom-checkbox">
-        <input type="checkbox" class="custom-control-input checkbox" id="FindDisabledForms" name="FindDisabledForms" value="1" <% $FindDisabledForms ? 'checked="checked"': '' |n%> />
-        <label class="custom-control-label" for="FindDisabledForms"><&|/l&>Include disabled forms in listing.</&></label>
-      </div>
-    </div>
-    <div class="col-auto">
-      <input type="submit" name="Go" class="form-control btn btn-primary" value="<&|/l&>Go!</&>" />
-    </div>
-  </div>
   <table class="table collection collection-as-table" cellspacing="0">
     <tbody>
       <tr class="collection-as-table">
@@ -38,15 +28,23 @@
 % }
     </tbody>
   </table>
-  <div class="form-row">
+  <div class="form-row align-items-center">
     <div class="col-12">
-      <div align="right">
+      <div class="custom-control custom-checkbox text-right">
+        <input type="checkbox" class="custom-control-input checkbox" id="FindDisabledForms" name="FindDisabledForms" value="1" <% $FindDisabledForms ? 'checked="checked"': '' |n%> />
+        <label class="custom-control-label" for="FindDisabledForms"><&|/l&>Include disabled forms in listing.</&></label>
+      </div>
+    </div>
+  </div>
+  <div class="form-row align-items-center">
+    <div class="col-12">
+      <div class="text-right">
         <input type="submit" name="Submit" class="form-control btn btn-primary" value="<&|/l&>Update</&>" />
       </div>
     </div>
   </div>
 </form>
-
+</&>
 <%init>
 my @results;
 

commit 5d61baee7d6a89449646f6c8c49a111004c07d19
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Tue Oct 17 09:43:25 2023 -0400

    Put the container bootstrap class directly on titlebox
    
    Also update max-width to a percent so the titlebox will
    be more responsive on smaller screens.

diff --git a/html/Elements/Forms/ListForms b/html/Elements/Forms/ListForms
index 575276c..c7afbfd 100644
--- a/html/Elements/Forms/ListForms
+++ b/html/Elements/Forms/ListForms
@@ -1,5 +1,4 @@
-<&|/Widgets/TitleBox, title => '', class => 'fullwidth formtools-form-list ml-auto mr-auto' &>
-<div class="container">
+<&|/Widgets/TitleBox, title => '', class => 'container formtools-form-list ml-auto mr-auto' &>
 % my $item_number = 1;
 % while ( my $form_attribute = $forms->Next ) {
 %    my $form = $form_attribute->Content;
@@ -17,7 +16,6 @@
 <hr />
 % $item_number++;
 % }
-</div>
 </&>
 <%init>
 my $forms = RT::Attributes->new( RT->SystemUser );
diff --git a/static/css/rt-extension-formtools.css b/static/css/rt-extension-formtools.css
index b4ccb41..575b09c 100644
--- a/static/css/rt-extension-formtools.css
+++ b/static/css/rt-extension-formtools.css
@@ -57,7 +57,7 @@ p.formtools-form-graphic-text {
 }
 
 div .formtools-form-list {
-    max-width: 1000px;
+    max-width: 85%;
 }
 
 div .formtools-admin-description {

commit 256c760d1427b9cefe191706a63dd098af25e009
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Tue Oct 17 09:30:36 2023 -0400

    Improve layout on Forms home page
    
    Default the background color when there is no icon rather
    than assigning from a list of colors. Center text vertically,
    make it black, and make it a bit bigger. Also add an HR
    to more clearly separate each form.

diff --git a/html/Elements/Forms/ListForms b/html/Elements/Forms/ListForms
index b0ecbd7..575276c 100644
--- a/html/Elements/Forms/ListForms
+++ b/html/Elements/Forms/ListForms
@@ -10,12 +10,11 @@
       <% $m->scomp('/Elements/Forms/ShowFormGraphic', FormObj => $form_attribute, ItemNumber => $item_number) |n %>
     </a>
   </div>
-  <div class="col">
-    <div class="formtools-form-text w-100 mt-2 mb-2 mr-auto ml-auto">
-      <% $form->{'form-description'} |n %>
-    </div>
+  <div class="col my-auto ml-2 mr-2 formtools-form-text">
+    <% $form->{'form-description'} |n %>
   </div>
 </div>
+<hr />
 % $item_number++;
 % }
 </div>
diff --git a/html/Elements/Forms/ShowFormGraphic b/html/Elements/Forms/ShowFormGraphic
index fd14ab8..348c76e 100644
--- a/html/Elements/Forms/ShowFormGraphic
+++ b/html/Elements/Forms/ShowFormGraphic
@@ -2,8 +2,8 @@
 % if ( $has_icon ) {
 <& /Elements/Forms/ShowFormIcon, FormID => $FormObj->Id &>
 % } else {
-  <div class="rounded pt-5 pb-5" style="background-color: <%$color%>;">
-    <p class="text-center formtools-form-graphic">
+  <div class="formtools-form-graphic-box border border-primary rounded pt-4 pb-4">
+    <p class="text-center formtools-form-graphic-text pl-1 pr-1">
       <% $FormObj->Description %>
     </p>
   </div>
@@ -16,13 +16,6 @@ unless ( $FormObj and $FormObj->Id ) {
     return;
 }
 
-my $color_list = [
-    "#1f77b4", "#ff7f0e", "#2ca02c", "#d62728",
-    "#9467bd", "#8c564b", "#e377c2", "#7f7f7f"
-];
-
-my $color = $color_list->[ $ItemNumber % 8 ];
-
 my $has_icon = 0;
 my $form_icon = RT::Attribute->new( RT->SystemUser );
 my ($ok, $msg) = $form_icon->LoadByCols( ObjectType => 'RT::Attribute', ObjectId => $FormObj->Id );
diff --git a/static/css/rt-extension-formtools.css b/static/css/rt-extension-formtools.css
index 743981e..b4ccb41 100644
--- a/static/css/rt-extension-formtools.css
+++ b/static/css/rt-extension-formtools.css
@@ -47,7 +47,11 @@ div .formtools-content {
     opacity: 0.7;
 }
 
-p.formtools-form-graphic {
+.formtools-form-graphic-box {
+    background-color: #EEEEFF;
+}
+
+p.formtools-form-graphic-text {
     font-weight: bold;
     font-size: 1.5rem;
 }
@@ -59,3 +63,8 @@ div .formtools-form-list {
 div .formtools-admin-description {
     max-width: 1000px;
 }
+
+.formtools-form-text > p {
+    color: black;
+    line-height: 1.5;
+}

commit 2fa490ea3b9ac2430b02a4e39de95b33c761ec6d
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Mon Oct 16 15:54:53 2023 -0400

    Add a BeforeCreate callback

diff --git a/html/Forms/dhandler b/html/Forms/dhandler
index fbcfb22..9a73a1d 100644
--- a/html/Forms/dhandler
+++ b/html/Forms/dhandler
@@ -156,6 +156,9 @@ if ( $ARGS{Back} and $ARGS{Back} eq 'Back' ) {
 my ($ticket_obj, @results);
 if ( $create_ticket ) {
 
+    $m->callback( CallbackName => 'BeforeCreate', ARGSRef => \%ARGS, results => \@results,
+                  FormName => $form_name, PageName => $page );
+
     # We override Abort elsewhere so we'll get $ticket_obj here even if
     # the current user has no rights to see the newly created ticket.
 

commit 85bebb05622ba7610de7ef580913588efe6bcd6d
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Mon Oct 16 15:14:00 2023 -0400

    Add a Back button to form pages

diff --git a/html/Forms/dhandler b/html/Forms/dhandler
index c1c78c9..fbcfb22 100644
--- a/html/Forms/dhandler
+++ b/html/Forms/dhandler
@@ -32,7 +32,7 @@ foreach my $element ( @{$form_config->{'formtools-pages'}{$page}{'content'}} ) {
 </%perl>
 
 % if ( $form_config->{'formtools-pages'}{$page}{'next'} ) {
- <& /FormTools/Next, Label => $button_label &>
+ <& /FormTools/Next, Label => $button_label, Back => $show_back &>
 % }
 </&>
 <%init>
@@ -98,10 +98,6 @@ $m->notes( page_title => $form_config->{'formtools-pages'}{$page}{'name'} );
 my $base_path = '/Forms/';
 $base_path = '/SelfService' . $base_path if $SelfService;
 
-# Try to create a ticket if we're on the last page and
-# "create_ticket" is submitted as an arg from the second-to-last
-# page.
-
 my $validation = $form_config->{'formtools-pages'}{$page}{'validation'};
 
 if ( $validation and not $ARGS{'validation_ok'} ) {
@@ -109,6 +105,54 @@ if ( $validation and not $ARGS{'validation_ok'} ) {
     $create_ticket = 0;
 }
 
+my @form_pages
+    = sort { ( $form_config->{'formtools-pages'}{$a}{sort_order} || 0 ) <=> ( $form_config->{'formtools-pages'}{$b}{sort_order} || 0 ) }
+    keys %{ $form_config->{'formtools-pages'} };
+
+my $show_back = 0;
+my $index = 0;
+my $back_page;
+
+# When validation is enabled for this page, it submits to itself
+# first, so the back button only needs to go back 1. Otherwise,
+# current page was "next" on submit, so we need to go back 2.
+my $go_back_count = $validation ? 1 : 2;
+
+foreach my $page_index ( @form_pages ) {
+    if ( $page_index eq $page ) {
+        $back_page = $form_pages[$index - $go_back_count];
+        last;
+    }
+    $index++;
+}
+
+# Don't show the back button on the first page
+$show_back = 1 if $index > 0;
+
+# Did the user click Back?
+if ( $ARGS{Back} and $ARGS{Back} eq 'Back' ) {
+    # Avoid trying to go back again
+    delete $ARGS{Back};
+
+    # Don't create a ticket when going back
+    delete $ARGS{'create_ticket'} if $ARGS{'create_ticket'};
+    $create_ticket = 0;
+
+    $_form_tools_next = '';
+    delete $ARGS{'_form_tools_next'} if $ARGS{'_form_tools_next'};
+
+    # Redirect to the previous page
+    MaybeRedirectForResults(
+        Path      => $base_path . $form_name . '/' . $back_page,
+        Arguments => \%ARGS,
+        Force     => 1,
+    );
+}
+
+# Try to create a ticket if we're on the last page and
+# "create_ticket" is submitted as an arg from the second-to-last
+# page.
+
 my ($ticket_obj, @results);
 if ( $create_ticket ) {
 

commit f4f6ea00b5f6a972446f83fed6f64c8c37ff31f4
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Mon Oct 16 14:30:10 2023 -0400

    Show Next for button label until Submit page

diff --git a/html/Forms/dhandler b/html/Forms/dhandler
index 668ffa5..c1c78c9 100644
--- a/html/Forms/dhandler
+++ b/html/Forms/dhandler
@@ -9,6 +9,7 @@
 <%perl>
 # Build the current page here dyamically from config
 
+my $button_label = loc('Next');
 foreach my $element ( @{$form_config->{'formtools-pages'}{$page}{'content'}} ) {
     if ( $element->{type} eq 'raw_html' ) {
         $m->out( $element->{html} );
@@ -20,11 +21,18 @@ foreach my $element ( @{$form_config->{'formtools-pages'}{$page}{'content'}} ) {
     elsif ( $element->{type} eq 'component' ) {
         $m->comp('/FormTools/' . $element->{comp_name}, %{$element->{arguments}});
     }
+
+    if ( $element->{type} eq 'hidden'
+         and $element->{'input-name'} eq 'create_ticket' ) {
+
+        # This is the page that will submit, so change the button label
+        $button_label = loc('Submit');
+    }
 }
 </%perl>
 
 % if ( $form_config->{'formtools-pages'}{$page}{'next'} ) {
- <& /FormTools/Next &>
+ <& /FormTools/Next, Label => $button_label &>
 % }
 </&>
 <%init>

commit 7611d1c995ef3b0abc932a5d7ff22d82a9da1505
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Mon Oct 16 13:53:24 2023 -0400

    Add a Forms Home page for privileged users
    
    Sending privileged users to /SelfService/Forms.html caused
    the SelfService menu to be displayed, which would be
    confusing for privileged users.

diff --git a/html/Callbacks/FormTools/Elements/Tabs/Privileged b/html/Callbacks/FormTools/Elements/Tabs/Privileged
index ab2b30c..7c48c7b 100644
--- a/html/Callbacks/FormTools/Elements/Tabs/Privileged
+++ b/html/Callbacks/FormTools/Elements/Tabs/Privileged
@@ -30,6 +30,12 @@ if ( $m->request_path =~ m{^/Admin/FormTools/} ) {
     }
 }
 
+# Forms home page for all privileged users
+my $formtools = Menu->child('home')->child(
+    formtools => title => loc('Forms'),
+    path      => '/Forms.html',
+);
+
 </%init>
 <%args>
 $Path
diff --git a/html/Forms.html b/html/Forms.html
new file mode 100644
index 0000000..dc6cd3c
--- /dev/null
+++ b/html/Forms.html
@@ -0,0 +1,4 @@
+<& /Elements/Header, Title => loc("Forms Home") &>
+<& /Elements/Tabs &>
+
+<& /Elements/Forms/ListForms &>

commit a5121cfe2572bd4343034a07b9c42ae235368fee
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Fri Oct 13 16:34:35 2023 -0400

    Move modal out of draggable divs to make mouse clicks work in modal
    
    On some browsers like Firefox, previously you couldn't click in a
    textarea to move cursor because the modal was in a draggable parent.

diff --git a/html/Admin/FormTools/Modify.html b/html/Admin/FormTools/Modify.html
index daf4440..e53d21e 100644
--- a/html/Admin/FormTools/Modify.html
+++ b/html/Admin/FormTools/Modify.html
@@ -18,41 +18,41 @@
               <span class="fas fa-pencil-alt" alt="<% loc('Edit') %>" data-toggle="tooltip" data-placement="top" data-original-title="<% loc('Edit') %>"></span>
             </a>
 %           }
-            <a href="#" class="remove" onclick="jQuery(this).find('[data-toggle=tooltip]').tooltip('hide'); this.closest('.formtools-element').remove(); return false;">
+            <a href="#" class="remove" onclick="return formTools.deleteElement(event);">
               <span class="far fa-times-circle" alt="<% loc('Remove') %>" data-toggle="tooltip" data-placement="top" data-original-title="<% loc('Remove') %>"></span>
             </a>
           </p>
-%         if ( $item ne 'hr' ) {
-          <div class="modal fade formtools-element-modal" id="formtools-element-<% $item %>-modal" tabindex="-1" role="dialog">
-            <div class="modal-dialog" role="document">
-              <div class="modal-content">
-                <form class="formtools-element-form">
-                  <div class="modal-header">
-                    <h5 class="modal-title"><% loc('Modify Element') %></h5>
-                    <a href="javascript:void(0)" class="close" data-dismiss="modal" aria-label="Close">
-                      <span aria-hidden="true">×</span>
-                    </a>
-                  </div>
-                  <div class="modal-body">
-                    <&| /Elements/LabeledValue, Label => loc("Content") &>
+        </div>
+%       if ( $item ne 'hr' ) {
+        <div class="modal fade formtools-element-modal" id="formtools-element-<% $item %>-modal" tabindex="-1" role="dialog">
+          <div class="modal-dialog" role="document">
+            <div class="modal-content">
+              <form class="formtools-element-form">
+                <div class="modal-header">
+                  <h5 class="modal-title"><% loc('Modify Element') %></h5>
+                  <a href="javascript:void(0)" class="close" data-dismiss="modal" aria-label="Close">
+                    <span aria-hidden="true">×</span>
+                  </a>
+                </div>
+                <div class="modal-body">
+                  <&| /Elements/LabeledValue, Label => loc("Content") &>
 %                   if ( $item =~ /^h\d/ ) {
-                      <input name="content" class="form-control" data-wrapper="<% $item %>" />
+                    <input name="content" class="form-control" data-wrapper="<% $item %>" />
 %                   } elsif ( $item eq 'p' ) {
-                      <textarea name="content" class="form-control" data-wrapper="<% $item %>"></textarea>
+                    <textarea name="content" class="form-control" data-wrapper="<% $item %>"></textarea>
 %                   } elsif ( $item eq 'html' ) {
-                      <textarea name="content" class="form-control"></textarea>
+                    <textarea name="content" class="form-control"></textarea>
 %                   }
-                    </&>
-                  </div>
-                  <div class="modal-footer">
-                    <button type="submit" class="btn btn-primary button form-control"><% loc('Save') %></button>
-                  </div>
-                </form>
-              </div>
+                  </&>
+                </div>
+                <div class="modal-footer">
+                  <button type="submit" class="btn btn-primary button form-control"><% loc('Save') %></button>
+                </div>
+              </form>
             </div>
           </div>
-%         }
         </div>
+%       }
 % }
         <hr />
 % foreach my $item ( @core_components ) {
@@ -68,32 +68,32 @@
               <span class="far fa-times-circle" alt="<% loc('Remove') %>" data-toggle="tooltip" data-placement="top" data-original-title="<% loc('Remove') %>"></span>
             </a>
           </p>
-          <div class="modal fade formtools-element-modal" id="formtools-element-<% $item %>-modal" tabindex="-1" role="dialog">
-            <div class="modal-dialog" role="document">
-              <div class="modal-content">
-                <form class="formtools-element-form">
-                  <div class="modal-header">
-                    <h5 class="modal-title"><% loc('Modify Element') %></h5>
-                    <a href="javascript:void(0)" class="close" data-dismiss="modal" aria-label="Close">
-                      <span aria-hidden="true">×</span>
-                    </a>
-                  </div>
-                  <div class="modal-body">
-                    <&| /Elements/LabeledValue, Label => loc('Label') &>
-                      <input name="label" type="text" class="form-control" placeholder="<% $item %>" value="" />
-                    </&>
-                    <&| /Elements/LabeledValue, Label => loc('Default Value') &>
-                      <input name="default" type="text" class="form-control" placeholder="<% $default_values{$item} %>" value="" />
-                    </&>
-                    <&| /Elements/LabeledValue, Label => loc('Tooltip') &>
-                      <input name="tooltip" type="text" class="form-control" value="" />
-                    </&>
-                  </div>
-                  <div class="modal-footer">
-                    <button type="submit" class="btn btn-primary button form-control"><% loc('Save') %></button>
-                  </div>
-                </form>
-              </div>
+        </div>
+        <div class="modal fade formtools-element-modal" id="formtools-element-<% $item %>-modal" tabindex="-1" role="dialog">
+          <div class="modal-dialog" role="document">
+            <div class="modal-content">
+              <form class="formtools-element-form">
+                <div class="modal-header">
+                  <h5 class="modal-title"><% loc('Modify Element') %></h5>
+                  <a href="javascript:void(0)" class="close" data-dismiss="modal" aria-label="Close">
+                    <span aria-hidden="true">×</span>
+                  </a>
+                </div>
+                <div class="modal-body">
+                  <&| /Elements/LabeledValue, Label => loc('Label') &>
+                    <input name="label" type="text" class="form-control" placeholder="<% $item %>" value="" />
+                  </&>
+                  <&| /Elements/LabeledValue, Label => loc('Default Value') &>
+                    <input name="default" type="text" class="form-control" placeholder="<% $default_values{$item} %>" value="" />
+                  </&>
+                  <&| /Elements/LabeledValue, Label => loc('Tooltip') &>
+                    <input name="tooltip" type="text" class="form-control" value="" />
+                  </&>
+                </div>
+                <div class="modal-footer">
+                  <button type="submit" class="btn btn-primary button form-control"><% loc('Save') %></button>
+                </div>
+              </form>
             </div>
           </div>
         </div>
@@ -112,40 +112,40 @@
               <span class="far fa-times-circle" alt="<% loc('Remove') %>" data-toggle="tooltip" data-placement="top" data-original-title="<% loc('Remove') %>"></span>
             </a>
           </p>
-          <div class="modal fade formtools-element-modal" id="formtools-element-<% $item %>-modal" tabindex="-1" role="dialog">
-            <div class="modal-dialog" role="document">
-              <div class="modal-content">
-                <form class="formtools-element-form">
-                  <div class="modal-header">
-                    <h5 class="modal-title"><% loc('Modify Element') %></h5>
-                    <a href="javascript:void(0)" class="close" data-dismiss="modal" aria-label="Close">
-                      <span aria-hidden="true">×</span>
-                    </a>
-                  </div>
-                  <div class="modal-body">
-                    <&| /Elements/LabeledValue, Label => loc('Label') &>
-                      <input name="label" type="text" class="form-control" placeholder="<% $item %>" value="" />
-                    </&>
-                    <&| /Elements/LabeledValue, Label => loc('Default Value') &>
-                      <input name="default" type="text" class="form-control" placeholder="<% $default_values{$item} %>" value="" />
-                    </&>
-                    <&| /Elements/LabeledValue, Label => loc('Tooltip') &>
-                      <input name="tooltip" type="text" class="form-control" placeholder="<% $tooltips{$item} // '' %>" value="" />
-                    </&>
-                    <&| /Elements/LabeledValue, Label => '' &>
-                      <div class="custom-control custom-checkbox">
-                        <input class="custom-control-input" id="<% CSSClass($item) %>-validation" type="checkbox" name="show_validation" value="1" checked="checked" />
-                        <label class="custom-control-label" for="<% CSSClass($item) %>-validation">
-                          <&|/l&>Show validation</&>
-                        </label>
-                      </div>
-                    </&>
-                  </div>
-                  <div class="modal-footer">
-                    <button type="submit" class="btn btn-primary button form-control"><% loc('Save') %></button>
-                  </div>
-                </form>
-              </div>
+        </div>
+        <div class="modal fade formtools-element-modal" id="formtools-element-<% $item %>-modal" tabindex="-1" role="dialog">
+          <div class="modal-dialog" role="document">
+            <div class="modal-content">
+              <form class="formtools-element-form">
+                <div class="modal-header">
+                  <h5 class="modal-title"><% loc('Modify Element') %></h5>
+                  <a href="javascript:void(0)" class="close" data-dismiss="modal" aria-label="Close">
+                    <span aria-hidden="true">×</span>
+                  </a>
+                </div>
+                <div class="modal-body">
+                  <&| /Elements/LabeledValue, Label => loc('Label') &>
+                    <input name="label" type="text" class="form-control" placeholder="<% $item %>" value="" />
+                  </&>
+                  <&| /Elements/LabeledValue, Label => loc('Default Value') &>
+                    <input name="default" type="text" class="form-control" placeholder="<% $default_values{$item} %>" value="" />
+                  </&>
+                  <&| /Elements/LabeledValue, Label => loc('Tooltip') &>
+                    <input name="tooltip" type="text" class="form-control" placeholder="<% $tooltips{$item} // '' %>" value="" />
+                  </&>
+                  <&| /Elements/LabeledValue, Label => '' &>
+                    <div class="custom-control custom-checkbox">
+                      <input class="custom-control-input" id="<% CSSClass($item) %>-validation" type="checkbox" name="show_validation" value="1" checked="checked" />
+                      <label class="custom-control-label" for="<% CSSClass($item) %>-validation">
+                        <&|/l&>Show validation</&>
+                      </label>
+                    </div>
+                  </&>
+                </div>
+                <div class="modal-footer">
+                  <button type="submit" class="btn btn-primary button form-control"><% loc('Save') %></button>
+                </div>
+              </form>
             </div>
           </div>
         </div>
@@ -167,35 +167,34 @@
               <span class="far fa-times-circle" alt="<% loc('Remove') %>" data-toggle="tooltip" data-placement="top" data-original-title="<% loc('Remove') %>"></span>
             </a>
           </p>
-
-%         if ( $item eq 'Hidden' ) {
-          <div class="modal fade formtools-element-modal" id="formtools-element-<% $item %>-modal" tabindex="-1" role="dialog">
-            <div class="modal-dialog" role="document">
-              <div class="modal-content">
-                <form class="formtools-element-form">
-                  <div class="modal-header">
-                    <h5 class="modal-title"><% loc('Modify Element') %></h5>
-                    <a href="javascript:void(0)" class="close" data-dismiss="modal" aria-label="Close">
-                      <span aria-hidden="true">×</span>
-                    </a>
-                  </div>
-                  <div class="modal-body">
-                    <&| /Elements/LabeledValue, Label => loc('Name') &>
-                      <input name="name" type="text" class="form-control" value="" />
-                    </&>
-                    <&| /Elements/LabeledValue, Label => loc('Value') &>
-                      <input name="value" type="text" class="form-control" value="" />
-                    </&>
-                  </div>
-                  <div class="modal-footer">
-                    <button type="submit" class="btn btn-primary button form-control"><% loc('Save') %></button>
-                  </div>
-                </form>
-              </div>
+        </div>
+%       if ( $item eq 'Hidden' ) {
+        <div class="modal fade formtools-element-modal" id="formtools-element-<% $item %>-modal" tabindex="-1" role="dialog">
+          <div class="modal-dialog" role="document">
+            <div class="modal-content">
+              <form class="formtools-element-form">
+                <div class="modal-header">
+                  <h5 class="modal-title"><% loc('Modify Element') %></h5>
+                  <a href="javascript:void(0)" class="close" data-dismiss="modal" aria-label="Close">
+                    <span aria-hidden="true">×</span>
+                  </a>
+                </div>
+                <div class="modal-body">
+                  <&| /Elements/LabeledValue, Label => loc('Name') &>
+                    <input name="name" type="text" class="form-control" value="" />
+                  </&>
+                  <&| /Elements/LabeledValue, Label => loc('Value') &>
+                    <input name="value" type="text" class="form-control" value="" />
+                  </&>
+                </div>
+                <div class="modal-footer">
+                  <button type="submit" class="btn btn-primary button form-control"><% loc('Save') %></button>
+                </div>
+              </form>
             </div>
           </div>
-%         }
         </div>
+%       }
 % }
       </div>
     </&>
@@ -306,72 +305,75 @@
                   <span class="far fa-times-circle" alt="<% loc('Remove') %>" data-toggle="tooltip" data-placement="top" data-original-title="<% loc('Remove') %>"></span>
                 </a>
               </p>
-              <div class="modal fade formtools-element-modal" id="formtools-element-<% $form_page_id{$page_name} %>-<% $i %>-modal" tabindex="-1" role="dialog">
-                <div class="modal-dialog" role="document">
-                  <div class="modal-content">
-                    <form class="formtools-element-form">
-                      <div class="modal-header">
-                        <h5 class="modal-title"><% loc('Modify Element') %></h5>
-                        <a href="javascript:void(0)" class="close" data-dismiss="modal" aria-label="Close">
-                          <span aria-hidden="true">×</span>
-                        </a>
-                      </div>
-                      <div class="modal-body">
-%                     if ( $item->{type} eq 'raw_html' ) {
-                        <&| /Elements/LabeledValue, Label => loc("Content") &>
-%                       if ( ($item->{wrapper} // '') =~ /^h\d/i ) {
-                          <input name="content" class="form-control" data-wrapper="<% $item->{wrapper} %>" value="<% $item->{content} %>"/>
-%                       } elsif ( lc ($item->{wrapper} // '') eq 'p' ) {
-                          <textarea name="content" class="form-control" data-wrapper="<% $item->{wrapper} %>"><% $item->{content} %></textarea>
-%                       } elsif ( $item eq 'html' ) {
-                          <textarea name="content" class="form-control"><% $item->{html} %></textarea>
-%                       }
-                        </&>
-%                     } elsif ( $item->{type} eq 'component' && $item->{comp_name} eq 'Field' ) {
-                        <&| /Elements/LabeledValue, Label => loc('Label') &>
-                          <input name="label" type="text" class="form-control" placeholder="<% $item->{arguments}{name} %>" value="<% $item->{arguments}{label} // ''  %>" />
-                        </&>
-                        <&| /Elements/LabeledValue, Label => loc('Default Value') &>
-                          <input name="default" type="text" class="form-control" placeholder="<% $default_values{$item->{arguments}{name}} %>" value="<% $item->{arguments}{default} // '' %>" />
-                        </&>
-
-                        <&| /Elements/LabeledValue, Label => loc('Tooltip') &>
-                          <input name="tooltip" type="text" class="form-control" placeholder="<% $tooltips{$item->{arguments}{name}} // '' %>" value="<% $item->{arguments}{tooltip} // '' %>" />
-                        </&>
-%                       if ( !RT::Extension::FormTools::is_core_field($item->{arguments}{name}) ) {
-                        <&| /Elements/LabeledValue, Label => '' &>
-                          <div class="custom-control custom-checkbox">
-                            <input class="custom-control-input" id="formtools-element-<% $form_page_id{$page_name} %>-<% $i %>-validation" type="checkbox" name="show_validation" value="1" <% $item->{arguments}{show_validation} ? q{checked="checked"} : '' |n %> />
-                            <label class="custom-control-label" for="formtools-element-<% $form_page_id{$page_name} %>-<% $i %>-validation">
-                              <&|/l&>Show validation</&>
-                            </label>
-                          </div>
-                        </&>
-%                       }
-%                     } elsif ( $item->{type} eq 'hidden' ) {
-                        <&| /Elements/LabeledValue, Label => loc('Name') &>
-                          <input name="name" type="text" class="form-control" value="<% $item->{'input-name'} // ''  %>" />
-                        </&>
-                        <&| /Elements/LabeledValue, Label => loc('Value') &>
-                          <input name="value" type="text" class="form-control" value="<% $item->{'input-value'} // '' %>" />
-                        </&>
+            </div>
+%           $i++;
+%         }
+            <p class="formtools-element-placeholder m-1 p-2 border rounded"><% loc('Put it here') %></p>
+          </div>
+          <div class="modal-wrapper">
+%         $i = 0;
+%         for my $item ( grep { $_->{type} ne 'hidden' || $_->{'input-name'} ne 'create_ticket' } @{$form->{'formtools-pages'}{$page_name}{content} || []} ) {
+            <div class="modal fade formtools-element-modal" id="formtools-element-<% $form_page_id{$page_name} %>-<% $i %>-modal" tabindex="-1" role="dialog">
+              <div class="modal-dialog" role="document">
+                <div class="modal-content">
+                  <form class="formtools-element-form">
+                    <div class="modal-header">
+                      <h5 class="modal-title"><% loc('Modify Element') %></h5>
+                      <a href="javascript:void(0)" class="close" data-dismiss="modal" aria-label="Close">
+                        <span aria-hidden="true">×</span>
+                      </a>
+                    </div>
+                    <div class="modal-body">
+%                   if ( $item->{type} eq 'raw_html' ) {
+                      <&| /Elements/LabeledValue, Label => loc("Content") &>
+%                     if ( ($item->{wrapper} // '') =~ /^h\d/i ) {
+                        <input name="content" class="form-control" data-wrapper="<% $item->{wrapper} %>" value="<% $item->{content} %>"/>
+%                     } elsif ( lc ($item->{wrapper} // '') eq 'p' ) {
+                        <textarea name="content" class="form-control" data-wrapper="<% $item->{wrapper} %>"><% $item->{content} %></textarea>
+%                     } elsif ( $item eq 'html' ) {
+                        <textarea name="content" class="form-control"><% $item->{html} %></textarea>
 %                     }
-                      </div>
-                      <div class="modal-footer">
-
-                        <button type="submit" class="btn btn-primary button form-control"><% loc('Save') %></button>
-                      </div>
-                    </form>
-                  </div>
+                      </&>
+%                   } elsif ( $item->{type} eq 'component' && $item->{comp_name} eq 'Field' ) {
+                      <&| /Elements/LabeledValue, Label => loc('Label') &>
+                        <input name="label" type="text" class="form-control" placeholder="<% $item->{arguments}{name} %>" value="<% $item->{arguments}{label} // ''  %>" />
+                      </&>
+                      <&| /Elements/LabeledValue, Label => loc('Default Value') &>
+                        <input name="default" type="text" class="form-control" placeholder="<% $default_values{$item->{arguments}{name}} %>" value="<% $item->{arguments}{default} // '' %>" />
+                      </&>
+
+                      <&| /Elements/LabeledValue, Label => loc('Tooltip') &>
+                        <input name="tooltip" type="text" class="form-control" placeholder="<% $tooltips{$item->{arguments}{name}} // '' %>" value="<% $item->{arguments}{tooltip} // '' %>" />
+                      </&>
+%                     if ( !RT::Extension::FormTools::is_core_field($item->{arguments}{name}) ) {
+                      <&| /Elements/LabeledValue, Label => '' &>
+                        <div class="custom-control custom-checkbox">
+                          <input class="custom-control-input" id="formtools-element-<% $form_page_id{$page_name} %>-<% $i %>-validation" type="checkbox" name="show_validation" value="1" <% $item->{arguments}{show_validation} ? q{checked="checked"} : '' |n %> />
+                          <label class="custom-control-label" for="formtools-element-<% $form_page_id{$page_name} %>-<% $i %>-validation">
+                            <&|/l&>Show validation</&>
+                          </label>
+                        </div>
+                      </&>
+%                     }
+%                   } elsif ( $item->{type} eq 'hidden' ) {
+                      <&| /Elements/LabeledValue, Label => loc('Name') &>
+                        <input name="name" type="text" class="form-control" value="<% $item->{'input-name'} // ''  %>" />
+                      </&>
+                      <&| /Elements/LabeledValue, Label => loc('Value') &>
+                        <input name="value" type="text" class="form-control" value="<% $item->{'input-value'} // '' %>" />
+                      </&>
+%                   }
+                    </div>
+                    <div class="modal-footer">
+                      <button type="submit" class="btn btn-primary button form-control"><% loc('Save') %></button>
+                    </div>
+                  </form>
                 </div>
               </div>
             </div>
 %           $i++;
 %         }
-            <p class="formtools-element-placeholder m-1 p-2 border rounded"><% loc('Put it here') %></p>
           </div>
-
-
         </&>
       </div>
 %   }
diff --git a/static/js/rt-extension-formtools.js b/static/js/rt-extension-formtools.js
index 3b0fa87..a177839 100644
--- a/static/js/rt-extension-formtools.js
+++ b/static/js/rt-extension-formtools.js
@@ -53,10 +53,6 @@ formTools = {
 
             const old_id = source_copy.id;
             source_copy.id = 'formtools-element-' + area.dataset.pageId + '-' + Date.now();
-            jQuery(source_copy).find('#' + old_id + '-modal').attr('id', source_copy.id + '-modal' );
-            jQuery(source_copy).find('a.edit').attr('data-target', '#' + source_copy.id + '-modal' );
-            jQuery(source_copy).find('form.formtools-element-form').on('submit', formTools.elementSubmit);
-            jQuery(source_copy).find('.formtools-element-modal').modal('show');
             jQuery(source_copy).attr('ondragenter', 'formTools.dragenter(event);');
             if ( sibling ) {
                 area.insertBefore(source_copy, sibling);
@@ -64,6 +60,14 @@ formTools = {
             else {
                 area.insertBefore(source_copy, area.children[area.children.length-1]);
             }
+
+            const modal_copy = jQuery('#' + old_id + '-modal').clone(true);
+            jQuery('div.modal-wrapper:visible').append(modal_copy);
+            modal_copy.attr('id', source_copy.id + '-modal' );
+            modal_copy.find('a.edit').attr('data-target', '#' + source_copy.id + '-modal' );
+            modal_copy.find('form.formtools-element-form').on('submit', formTools.elementSubmit);
+            modal_copy.modal('show');
+            modal_copy.attr('ondragenter', 'formTools.dragenter(event);');
         }
     },
 
@@ -76,7 +80,8 @@ formTools = {
     elementSubmit: function(e) {
         e.preventDefault();
         const form = jQuery(this);
-        const element = form.closest('.formtools-element');
+        const modal = form.closest('.formtools-element-modal');
+        const element = jQuery('#' + modal.attr('id').replace(/-modal$/, ''));
         const value = element.data('value');
 
         if ( value.type === 'raw_html' ) {
@@ -164,6 +169,17 @@ formTools = {
         form.find('input[name=Content]').val(JSON.stringify(content));
     },
 
+    deleteElement: function(event) {
+        jQuery(event.target).find('[data-toggle=tooltip]').tooltip('hide');
+        const element = event.target.closest('.formtools-element');
+        const modal = document.getElementById(element.id + '-modal');
+        if ( modal ) {
+            modal.remove();
+        }
+        element.remove();
+        return false;
+    },
+
     deletePage: function() {
         jQuery('.formtools-element-modal.show').modal('hide');
         const tab = jQuery(this).closest('.tab-pane');

commit 841da6cc08527970a8b84ce88a2858deef7a0636
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Thu Oct 12 17:27:05 2023 -0400

    Hide disabled forms from end users

diff --git a/html/Elements/Forms/ListForms b/html/Elements/Forms/ListForms
index c72637e..b0ecbd7 100644
--- a/html/Elements/Forms/ListForms
+++ b/html/Elements/Forms/ListForms
@@ -3,6 +3,7 @@
 % my $item_number = 1;
 % while ( my $form_attribute = $forms->Next ) {
 %    my $form = $form_attribute->Content;
+%    next if $form->{disabled};
 <div class="row mt-3 mb-3">
   <div class="col-4">
     <a href="<% RT->Config->Get('WebPath') %><% $SelfServicePath %>/Forms/<% $form_attribute->Description %>">

commit 4c3f7620d79e7bb6183fb85e068c36802879eaf6
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Thu Oct 12 16:17:56 2023 -0400

    Support to enable/disable forms

diff --git a/html/Admin/FormTools/index.html b/html/Admin/FormTools/index.html
index 68f0913..3c00092 100644
--- a/html/Admin/FormTools/index.html
+++ b/html/Admin/FormTools/index.html
@@ -2,11 +2,24 @@
 <& /Elements/Tabs &>
 <& /Elements/ListActions, actions => \@results &>
 
+<form method="post" action="<% RT->Config->Get('WebPath') %>/Admin/FormTools/index.html">
+  <div class="form-row">
+    <div class="col-auto">
+      <div class="custom-control custom-checkbox">
+        <input type="checkbox" class="custom-control-input checkbox" id="FindDisabledForms" name="FindDisabledForms" value="1" <% $FindDisabledForms ? 'checked="checked"': '' |n%> />
+        <label class="custom-control-label" for="FindDisabledForms"><&|/l&>Include disabled forms in listing.</&></label>
+      </div>
+    </div>
+    <div class="col-auto">
+      <input type="submit" name="Go" class="form-control btn btn-primary" value="<&|/l&>Go!</&>" />
+    </div>
+  </div>
   <table class="table collection collection-as-table" cellspacing="0">
     <tbody>
       <tr class="collection-as-table">
         <th class="collection-as-table"><&|/l&>Name</&></th>
         <th class="collection-as-table"><&|/l&>Creates Tickets in Queue</&></th>
+        <th class="collection-as-table"><&|/l&>Enabled</&></th>
       </tr>
 % my $i = 0;
 % for my $form_ref ( @forms ) {
@@ -14,11 +27,25 @@
       <tr class="<% $i % 2 ? 'oddline' : 'evenline' %>">
         <td class="collection-as-table"><a href="<% RT->Config->Get('WebURL') %>Admin/FormTools/Modify.html?id=<% $form_ref->{'id'} %>"><% $form_ref->{'name'} %></a></td>
         <td class="collection-as-table"><% $form_ref->{'queue_name'} %></td>
+        <td class="collection-as-table">
+          <div class="custom-control custom-checkbox">
+            <input type="checkbox" class="custom-control-input checkbox" id="enable-form-<% $form_ref->{'id'} %>" name="enable-form-<% $form_ref->{'id'} %>" value="1" <% $form_ref->{disabled} ? '' : 'checked="checked"' %> />
+            <label class="custom-control-label" for="enable-form-<% $form_ref->{'id'} %>"></label>
+          </div>
+        </td>
       </tr>
 %    ++$i;
 % }
     </tbody>
   </table>
+  <div class="form-row">
+    <div class="col-12">
+      <div align="right">
+        <input type="submit" name="Submit" class="form-control btn btn-primary" value="<&|/l&>Update</&>" />
+      </div>
+    </div>
+  </div>
+</form>
 
 <%init>
 my @results;
@@ -27,8 +54,62 @@ my $forms = RT::Attributes->new( $session{'CurrentUser'} );
 $forms->Limit( FIELD => 'Name', VALUE => 'FormTools Form' );
 
 my @unsorted_forms;
+
+if ( $Submit ) {
+    while ( my $form = $forms->Next ) {
+        my $id = $form->Id;
+        my $content = $form->Content;
+        my $name = $form->Description;
+
+        my $changed;
+        if ( $ARGS{"enable-form-$id"} ) {
+            if ( $content->{disabled} ) {
+                $content->{disabled} = 0;
+                $changed = 1;
+            }
+        }
+        else {
+            if ( !$content->{disabled} ) {
+                $content->{disabled} = 1;
+                $changed = 1;
+            }
+        }
+
+        if ( $changed ) {
+            my ( $ret, $msg ) = $form->SetContent($content);
+            if ($ret) {
+                if ( $content->{disabled} ) {
+                    push @results, loc('Disabled [_1]', $name);
+                }
+                else {
+                    push @results, loc('Enabled [_1]', $name);
+                }
+            }
+            else {
+                if ( $content->{disabled} ) {
+                    push @results, loc( "Couldn't disable [_1]: [_2]", $name, $msg );
+                }
+                else {
+                    push @results, loc( "Couldn't enable [_1]: [_2]", $name, $msg );
+                }
+            }
+        }
+    }
+}
+
+
+if ( @results ) {
+    MaybeRedirectForResults(
+        Actions   => \@results,
+        Path      => '/Admin/FormTools/index.html',
+        Arguments => { FindDisabledForms => $FindDisabledForms },
+    );
+}
+
 while ( my $form = $forms->Next ) {
     my $form_ref = $form->Content;
+    next if !$FindDisabledForms && $form_ref->{disabled};
+
     $form_ref->{'name'} = $form->Description;
     $form_ref->{'id'} = $form->Id;
 
@@ -54,4 +135,6 @@ $m->callback(
 
 </%init>
 <%args>
+$Submit            => undef
+$FindDisabledForms => undef
 </%args>

commit 61d6f10fc456a778762a80c430a0b38588ebd5ba
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Thu Oct 12 12:42:03 2023 -0400

    Use current user to create forms so we know the actor

diff --git a/html/Admin/FormTools/Create.html b/html/Admin/FormTools/Create.html
index 062b839..58f2ed1 100644
--- a/html/Admin/FormTools/Create.html
+++ b/html/Admin/FormTools/Create.html
@@ -29,7 +29,7 @@ if ( $Create ) {
     push @results, loc('Missing Queue') unless $Queue;
 
     if ( $Description && $Queue ) {
-        my $form = RT::Attribute->new( RT->SystemUser );
+        my $form = RT::Attribute->new( $session{CurrentUser} );
         my ($ret) = $form->LoadByCols( Name => 'FormTools Form', Description => $Description );
         if ($ret) {
             push @results, loc( 'Name [_1] already exists', $Description );

commit c5dbf54e18b8e5c8dd845d5252018091ab0e34cb
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Wed Oct 11 17:36:54 2023 -0400

    Make sure orders are sorted on page create
    
    The sort order of the new page depends on the second to last item.

diff --git a/html/Admin/FormTools/Modify.html b/html/Admin/FormTools/Modify.html
index 7015b90..daf4440 100644
--- a/html/Admin/FormTools/Modify.html
+++ b/html/Admin/FormTools/Modify.html
@@ -418,7 +418,7 @@ if ( $AddPage ) {
     Abort( loc('Permission Denied') )
         unless $session{'CurrentUser'}->HasRight( Object => $RT::System, Right => 'SuperUser' );
 
-    my @orders = map { $form->{'formtools-pages'}{$_}{sort_order} } keys %{$form->{'formtools-pages'}};
+    my @orders = sort { $a <=> $b } map { $form->{'formtools-pages'}{$_}{sort_order} } keys %{$form->{'formtools-pages'}};
 
     my $new_page = RT::Extension::FormTools->GeneratePageId($form);
     $form->{'formtools-pages'}{$new_page} = {

commit 863cec90d45c0b691a8dad6fc159ea022e32833e
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Wed Oct 11 17:23:17 2023 -0400

    Add tooltip support to core and custom fields

diff --git a/html/Admin/FormTools/Modify.html b/html/Admin/FormTools/Modify.html
index d242379..7015b90 100644
--- a/html/Admin/FormTools/Modify.html
+++ b/html/Admin/FormTools/Modify.html
@@ -85,6 +85,9 @@
                     <&| /Elements/LabeledValue, Label => loc('Default Value') &>
                       <input name="default" type="text" class="form-control" placeholder="<% $default_values{$item} %>" value="" />
                     </&>
+                    <&| /Elements/LabeledValue, Label => loc('Tooltip') &>
+                      <input name="tooltip" type="text" class="form-control" value="" />
+                    </&>
                   </div>
                   <div class="modal-footer">
                     <button type="submit" class="btn btn-primary button form-control"><% loc('Save') %></button>
@@ -126,6 +129,9 @@
                     <&| /Elements/LabeledValue, Label => loc('Default Value') &>
                       <input name="default" type="text" class="form-control" placeholder="<% $default_values{$item} %>" value="" />
                     </&>
+                    <&| /Elements/LabeledValue, Label => loc('Tooltip') &>
+                      <input name="tooltip" type="text" class="form-control" placeholder="<% $tooltips{$item} // '' %>" value="" />
+                    </&>
                     <&| /Elements/LabeledValue, Label => '' &>
                       <div class="custom-control custom-checkbox">
                         <input class="custom-control-input" id="<% CSSClass($item) %>-validation" type="checkbox" name="show_validation" value="1" checked="checked" />
@@ -328,6 +334,10 @@
                         <&| /Elements/LabeledValue, Label => loc('Default Value') &>
                           <input name="default" type="text" class="form-control" placeholder="<% $default_values{$item->{arguments}{name}} %>" value="<% $item->{arguments}{default} // '' %>" />
                         </&>
+
+                        <&| /Elements/LabeledValue, Label => loc('Tooltip') &>
+                          <input name="tooltip" type="text" class="form-control" placeholder="<% $tooltips{$item->{arguments}{name}} // '' %>" value="<% $item->{arguments}{tooltip} // '' %>" />
+                        </&>
 %                       if ( !RT::Extension::FormTools::is_core_field($item->{arguments}{name}) ) {
                         <&| /Elements/LabeledValue, Label => '' &>
                           <div class="custom-control custom-checkbox">
@@ -493,6 +503,7 @@ $queue->Load($form->{queue});
 my $cfs = $queue->TicketCustomFields;
 my @custom_fields;
 my %default_values;
+my %tooltips;
 
 while ( my $cf = $cfs->Next ) {
     push @custom_fields, $cf->Name;
@@ -501,6 +512,7 @@ while ( my $cf = $cfs->Next ) {
             $default_values{$cf->Name} = ref $default_values eq 'ARRAY' ? join(', ', @$default_values) : $default_values;
         }
     }
+    $tooltips{$cf->Name} = $cf->EntryHint // '';
 }
 
 my %other_components = (
diff --git a/html/FormTools/Field b/html/FormTools/Field
index 2699834..f7694be 100644
--- a/html/FormTools/Field
+++ b/html/FormTools/Field
@@ -1,6 +1,7 @@
 <%args>
 $name
 $label              => undef
+$tooltip            => undef
 $item_labels        => {}
 $render_as          => 'normal'
 $default            => undef
@@ -59,6 +60,7 @@ if ( RT::Extension::FormTools::is_core_field($name) ) {
 
     $m->notes( cfs_on_page => [ @{$m->notes('cfs_on_page')||[]}, $cf->id ] );
     $field_label ||= $cf->Name;
+    $tooltip ||= $cf->EntryHint;
 
     if ($ticket) {
         @values = map { $_->Content } @{ $cf->ValuesForObject($ticket)->ItemsArrayRef };
@@ -125,7 +127,7 @@ $default = '' unless defined $default;
 
 % if ($render_as ne 'hidden' && $show_label) { # no label if hidden
 
-<&| /Elements/LabeledValue, RawLabel => $m->interp->apply_escapes($field_label, 'h') . $after_label &>
+<&| /Elements/LabeledValue, RawLabel => $m->interp->apply_escapes($field_label, 'h') . $after_label, LabelTooltip => $tooltip &>
   <& SELF:Value, %ARGS, input_name => $input_name, field_type => $field_type, default => $default, values => \@values, cf => $cf &>
   <% $after_input |n %>
 </&>
diff --git a/static/js/rt-extension-formtools.js b/static/js/rt-extension-formtools.js
index 8e9644c..3b0fa87 100644
--- a/static/js/rt-extension-formtools.js
+++ b/static/js/rt-extension-formtools.js
@@ -111,6 +111,15 @@ formTools = {
                 delete value.arguments.default;
             }
 
+            const tooltip = form.find(':input[name=tooltip]').val();
+
+            if ( tooltip.length ) {
+                value.arguments.tooltip = tooltip;
+            }
+            else {
+                delete value.arguments.tooltip;
+            }
+
             const validation = form.find(':input[name=show_validation]');
             if ( validation.length ) {
                 if ( validation.is(':checked') ) {

commit ca165b2f231ead2a88b2ae5862463807cc163ad8
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Tue Oct 10 15:08:09 2023 -0400

    Hide Submit button for result page

diff --git a/html/Forms/dhandler b/html/Forms/dhandler
index f8ca419..668ffa5 100644
--- a/html/Forms/dhandler
+++ b/html/Forms/dhandler
@@ -23,7 +23,9 @@ foreach my $element ( @{$form_config->{'formtools-pages'}{$page}{'content'}} ) {
 }
 </%perl>
 
-<& /FormTools/Next &>
+% if ( $form_config->{'formtools-pages'}{$page}{'next'} ) {
+ <& /FormTools/Next &>
+% }
 </&>
 <%init>
 

commit 8ad9f90ad8f85788454ee09d645f8139a1ca08b6
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Tue Oct 10 11:10:31 2023 -0400

    Switch to LabeledValue for end user pages

diff --git a/html/FormTools/Field b/html/FormTools/Field
index 5ffd333..2699834 100644
--- a/html/FormTools/Field
+++ b/html/FormTools/Field
@@ -124,11 +124,36 @@ $default = '' unless defined $default;
 % } else {
 
 % if ($render_as ne 'hidden' && $show_label) { # no label if hidden
-<div class="form-row">
-  <div class="label col-3"><%$field_label%><% $after_label |n %></div>
-  <div class="value col-9">
+
+<&| /Elements/LabeledValue, RawLabel => $m->interp->apply_escapes($field_label, 'h') . $after_label &>
+  <& SELF:Value, %ARGS, input_name => $input_name, field_type => $field_type, default => $default, values => \@values, cf => $cf &>
+  <% $after_input |n %>
+</&>
+
+% } else {
+  <& SELF:Value, %ARGS, input_name => $input_name, field_type => $field_type, default => $default, values => \@values, cf => $cf &>
+% }
+
 % }
 
+<%METHOD Value>
+<%ARGS>
+$name
+$render_as          => 'normal'
+$default            => undef
+$cols               => undef
+$rows               => undef
+$empty_allowed      => 1
+$show_validation    => 0
+$disables           => {}
+$after_input        => ''
+$cf                 => undef
+$queue              => undef
+$field_type         => undef
+$input_name         => undef
+ at values             => ()
+</%ARGS>
+
 % if ($field_type eq 'core') {
 
 % if ($name eq 'Attach') {
@@ -256,7 +281,4 @@ $default = '' unless defined $default;
 %}
 
 % }
-% if ($render_as ne 'hidden' && $show_label) {
-<% $after_input |n %></div></div>
-% }
-% }
+</%METHOD>
diff --git a/html/FormTools/ShowChoices b/html/FormTools/ShowChoices
index 176f306..8e07d15 100644
--- a/html/FormTools/ShowChoices
+++ b/html/FormTools/ShowChoices
@@ -9,22 +9,8 @@ my %all_fields = $m->request_args;
 % if ($field =~ /CustomField-(\d+)/) {
 % my $id = $1;
 %$cf->Load($id);
-<div class="form-row">
-  <div class="col-3 label"><%$cf->Name%></div>
-  <div class="col-9 value">
-    <span class="current-value">
-      <% ref ($all_fields{$field}) ? join(', ',@{$all_fields{$field}}) : $all_fields{$field}%>
-    </span>
-  </div>
-</div>
+<& /Elements/LabeledValue, Label => $cf->Name, ValueSpanClass => 'current-value', Value => ref ($all_fields{$field}) ? join(', ',@{$all_fields{$field}}) : $all_fields{$field} &>
 % } elsif (RT::Extension::FormTools::is_core_field($field)) {
-<div class="form-row">
-  <div class="col-3 label"><%$field%></div>
-  <div class="col-9 value">
-    <span class="current-value">
-      <% $all_fields{$field} %>
-    </span>
-  </div>
-</div>
+<& /Elements/LabeledValue, Label => loc("Content"), ValueSpanClass => 'current-value', Value => $all_fields{$field} &>
 %}
 % }

commit 523f857c07041523f4ce09c9efd64bc81f34e20f
Merge: 745ef34 37be40d
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Fri Oct 6 17:46:22 2023 -0400

    Merge branch 'improve-form-spacing' into dynamic-forms-from-config


commit 37be40da238d22c94e12923089b156d09e5f512a
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Fri Oct 6 16:55:12 2023 -0400

    Skip creating a ticket if validation fails

diff --git a/html/FormTools/Form b/html/FormTools/Form
index 3b7939d..e300468 100644
--- a/html/FormTools/Form
+++ b/html/FormTools/Form
@@ -64,6 +64,9 @@ if ($validation && $real_next) {
     }
 
     unless (@results) {
+        # Flag to show validation passed
+        $ARGS{'validation_ok'} = 1;
+
         $real_next = $m->caller(1)->dir_path . '/' . $real_next
             unless $real_next =~ m'^/';
         $m->subexec("$real_next", %ARGS, %request_args);
diff --git a/html/Forms/dhandler b/html/Forms/dhandler
index 4a600c4..f8ca419 100644
--- a/html/Forms/dhandler
+++ b/html/Forms/dhandler
@@ -92,6 +92,13 @@ $base_path = '/SelfService' . $base_path if $SelfService;
 # "create_ticket" is submitted as an arg from the second-to-last
 # page.
 
+my $validation = $form_config->{'formtools-pages'}{$page}{'validation'};
+
+if ( $validation and not $ARGS{'validation_ok'} ) {
+    # If validation is enabled and it didn't pass, don't create
+    $create_ticket = 0;
+}
+
 my ($ticket_obj, @results);
 if ( $create_ticket ) {
 

commit 745ef34354f4c10e4cb8de0115818cbfad47473e
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Fri Oct 6 15:56:36 2023 -0400

    Convert page basics form to modal to save space

diff --git a/html/Admin/FormTools/Modify.html b/html/Admin/FormTools/Modify.html
index 82162a2..d242379 100644
--- a/html/Admin/FormTools/Modify.html
+++ b/html/Admin/FormTools/Modify.html
@@ -216,34 +216,52 @@
     <div class="tab-content">
 %   foreach my $page_name (@form_pages) {
       <div id="formtools-content-<% $form_page_id{$page_name} %>" class="tab-pane <% $form_page_id{$page_name} eq $active_context->{tab} ? 'show active' : 'fade' %>" role="tabpanel">
-        <&| /Widgets/TitleBox, title => loc('Basic Info'), titleright_raw => qq{} &>
-        <form>
-          <&| /Elements/LabeledValue, Label => loc('Name') &>
-            <input name="name" class="form-control" value="<% $form->{'formtools-pages'}{$page_name}{name} %>" />
-          </&>
-          <&| /Elements/LabeledValue, Label => loc('Sort Order') &>
-            <input name="sort_order" class="form-control" value="<% $form->{'formtools-pages'}{$page_name}{sort_order} %>" />
-          </&>
-          <&| /Elements/LabeledValue, Label => '' &>
-            <div class="custom-control custom-checkbox">
-              <input class="custom-control-input" id="<% $form_page_id{$page_name} %>-validation" type="checkbox" name="validation" value="1" <% $form->{'formtools-pages'}{$page_name}{validation} ? 'checked="checked"' : '' |n%> />
-              <label class="custom-control-label" for="<% $form_page_id{$page_name} %>-validation">
-                <&|/l&>Enable validation</&>
-              </label>
-            </div>
-          </&>
+        <div class="modal fade formtools-element-modal" id="formtools-element-<% $form_page_id{$page_name} %>-basics-modal" tabindex="-1" role="dialog">
+          <div class="modal-dialog" role="document">
+            <div class="modal-content">
+              <form class="formtools-element-form">
+                <div class="modal-header">
+                  <h5 class="modal-title"><% loc('Modify Basics') %></h5>
+                  <a href="javascript:void(0)" class="close" data-dismiss="modal" aria-label="Close">
+                    <span aria-hidden="true">×</span>
+                  </a>
+                </div>
+                <div class="modal-body">
+                  <form>
+                    <&| /Elements/LabeledValue, Label => loc('Name') &>
+                      <input name="name" class="form-control" value="<% $form->{'formtools-pages'}{$page_name}{name} %>" />
+                    </&>
+                    <&| /Elements/LabeledValue, Label => loc('Sort Order') &>
+                      <input name="sort_order" class="form-control" value="<% $form->{'formtools-pages'}{$page_name}{sort_order} %>" />
+                    </&>
+                    <&| /Elements/LabeledValue, Label => '' &>
+                      <div class="custom-control custom-checkbox">
+                        <input class="custom-control-input" id="<% $form_page_id{$page_name} %>-validation" type="checkbox" name="validation" value="1" <% $form->{'formtools-pages'}{$page_name}{validation} ? 'checked="checked"' : '' |n%> />
+                        <label class="custom-control-label" for="<% $form_page_id{$page_name} %>-validation">
+                          <&|/l&>Enable validation</&>
+                        </label>
+                      </div>
+                    </&>
 
-%         # Do not delete the last one
-%         if ( $form->{'formtools-pages'}{$page_name}{next} ) {
-          <&| /Elements/LabeledValue, Label => '' &>
-            <a class="formtools-delete-page btn btn-primary button" data-page="<% $page_name %>" href="#"><% loc('Delete Page') %></a>
-          </&>
-%         }
-        </form>
-        </&>
+%                   # Do not delete the last one
+%                   if ( $form->{'formtools-pages'}{$page_name}{next} ) {
+                    <&| /Elements/LabeledValue, Label => '' &>
+                      <a class="formtools-delete-page btn btn-primary button" data-page="<% $page_name %>" href="#"><% loc('Delete Page') %></a>
+                    </&>
+%                   }
+                  </form>
+                </div>
+                <div class="modal-footer">
+                  <button type="submit" class="btn btn-primary button form-control" data-dismiss="modal"><% loc('Save') %></button>
+                </div>
+              </form>
+            </div>
+          </div>
+        </div>
 
-        <&| /Widgets/TitleBox, title => loc('Content') &>
-          <div class="formtools-content w-100 border rounded" data-page="<% $page_name %>" data-page-id="<% $form_page_id{$page_name} %>" ondrop="formTools.drop(event);" ondragover="formTools.dragover(event);" ondragend="formTools.dragend(event);">
+%       my $loc_basics = loc('Basics');
+        <&| /Widgets/TitleBox, title => loc('Content'), titleright_raw => qq{<a href="#" data-toggle="modal" data-target="#formtools-element-$form_page_id{$page_name}-basics-modal"><span class="fas fa-cog icon-bordered fa-2x" alt="$loc_basics" data-toggle="tooltip" data-placement="top" data-original-title="$loc_basics"></span></a>} &>
+          <div class="formtools-content w-100 border rounded mt-3" data-page="<% $page_name %>" data-page-id="<% $form_page_id{$page_name} %>" ondrop="formTools.drop(event);" ondragover="formTools.dragover(event);" ondragend="formTools.dragend(event);">
 %         my $i = 0;
 %         for my $item ( grep { $_->{type} ne 'hidden' || $_->{'input-name'} ne 'create_ticket' } @{$form->{'formtools-pages'}{$page_name}{content} || []} ) {
             <div id="formtools-element-<% $form_page_id{$page_name} %>-<% $i %>" class="formtools-element" draggable="true" ondragstart="formTools.dragstart(event);" ondragend="formTools.dragend(event);" ondragenter="formTools.dragenter(event)" ondragleave="formTools.dragleave(event)" data-value="<% JSON($item) %>">
diff --git a/static/js/rt-extension-formtools.js b/static/js/rt-extension-formtools.js
index d8055b7..8e9644c 100644
--- a/static/js/rt-extension-formtools.js
+++ b/static/js/rt-extension-formtools.js
@@ -156,12 +156,13 @@ formTools = {
     },
 
     deletePage: function() {
+        jQuery('.formtools-element-modal.show').modal('hide');
         const tab = jQuery(this).closest('.tab-pane');
-
         tab.fadeOut(function() {
             jQuery('#formtools-pages').find('a.nav-link[href="#' + tab.attr('id') + '"]').closest('li').remove();
             jQuery('#formtools-pages').find('li:first a.nav-link').tab('show');
-        }).remove();
+            tab.remove();
+        });
         return false;
     }
 };

commit 489f1a624aa1e94166caa05ae9c941a0d46ec24e
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Fri Oct 6 14:58:06 2023 -0400

    Build a non-SelfService link for Privileged users

diff --git a/html/Elements/Forms/ListForms b/html/Elements/Forms/ListForms
index f14f482..c72637e 100644
--- a/html/Elements/Forms/ListForms
+++ b/html/Elements/Forms/ListForms
@@ -5,7 +5,7 @@
 %    my $form = $form_attribute->Content;
 <div class="row mt-3 mb-3">
   <div class="col-4">
-    <a href="<% RT->Config->Get('WebPath') %>/SelfService/Forms/<% $form_attribute->Description %>">
+    <a href="<% RT->Config->Get('WebPath') %><% $SelfServicePath %>/Forms/<% $form_attribute->Description %>">
       <% $m->scomp('/Elements/Forms/ShowFormGraphic', FormObj => $form_attribute, ItemNumber => $item_number) |n %>
     </a>
   </div>
@@ -22,6 +22,12 @@
 <%init>
 my $forms = RT::Attributes->new( RT->SystemUser );
 $forms->Limit( FIELD => 'Name', VALUE => 'FormTools Form' );
+
+my $SelfServicePath = '/SelfService';
+
+if ( $session{'CurrentUser'}->Privileged ) {
+    $SelfServicePath = '';
+}
 </%init>
 <%args>
 </%args>

commit 7ea379bd4854ef3861b459e694ff9d16d9d32fe2
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Fri Oct 6 14:52:50 2023 -0400

    Show menus when displaying forms

diff --git a/html/Forms/dhandler b/html/Forms/dhandler
index 45da273..4a600c4 100644
--- a/html/Forms/dhandler
+++ b/html/Forms/dhandler
@@ -2,6 +2,8 @@
     validation => $form_config->{'formtools-pages'}{$page}{'validation'},
     next_for_validation => $base_path . $form_name . '/' . $page,
     results_ref => \@results,
+    self_service => $SelfService,
+    include_tabs => 1,
 &>
 
 <%perl>

commit 84c3dd1278592506cabbbb8e4d55ca047ed737ce
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Fri Oct 6 13:45:08 2023 -0400

    Improve spacing around displayed form

diff --git a/html/FormTools/Form b/html/FormTools/Form
index 58418b6..3b7939d 100644
--- a/html/FormTools/Form
+++ b/html/FormTools/Form
@@ -106,7 +106,7 @@ $next_for_validation ||= $m->caller(1)->path;
 % }
 
 <& /Elements/ListActions, actions => \@results &>
-
+<div id="formtools-form-body" class="container p-4 formtools-base-style">
 <form
     method="POST"
     action="<% $validation ? $next_for_validation : $next %>"
@@ -132,3 +132,4 @@ $next_for_validation ||= $m->caller(1)->path;
 % }
 
 </form>
+</div>
\ No newline at end of file
diff --git a/static/css/rt-extension-formtools.css b/static/css/rt-extension-formtools.css
index b5d719f..743981e 100644
--- a/static/css/rt-extension-formtools.css
+++ b/static/css/rt-extension-formtools.css
@@ -1,3 +1,6 @@
+#formtools-form-body {
+    max-width: 700px;
+}
 
 div .formtools-content {
     min-height: 400px;

commit 2dff55212f2f659cbce609bf7a46c78f7657caa7
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Fri Oct 6 14:05:08 2023 -0400

    Switch to LabeledValue for admin pages

diff --git a/html/Admin/FormTools/Advanced.html b/html/Admin/FormTools/Advanced.html
index 739c7a2..7605e5f 100644
--- a/html/Admin/FormTools/Advanced.html
+++ b/html/Admin/FormTools/Advanced.html
@@ -4,22 +4,12 @@
 
 <form action="<%RT->Config->Get('WebPath')%>/Admin/FormTools/Advanced.html" name="ModifyFormToolsAdvanced" method="post" enctype="multipart/form-data" class="mx-auto max-width-md">
   <input type="hidden" class="hidden" name="id" value="<% $id %>" />
-  <div class="form-row">
-    <div class="col-3 label">
-      <&|/l&>Name</&>:
-    </div>
-    <div class="col-9 value">
-      <input name="Description" class="form-control" value="<% $ARGS{Description} // $form_attribute->Description %>"/>
-    </div>
-  </div>
-  <div class="form-row">
-    <div class="col-3 label">
-      <&|/l&>Content</&>:
-    </div>
-    <div class="col-9 value">
-      <textarea class="form-control" rows="30" name="Content"><% $ARGS{Content} || $encoded_content %></textarea>
-    </div>
-  </div>
+  <&| /Elements/LabeledValue, Label => loc('Name') &>
+    <input name="Description" class="form-control" value="<% $ARGS{Description} // $form_attribute->Description %>"/>
+  </&>
+  <&| /Elements/LabeledValue, Label => loc('Content') &>
+    <textarea class="form-control" rows="30" name="Content"><% $ARGS{Content} || $encoded_content %></textarea>
+  </&>
 
   <div class="form-row">
     <div class="col-12">
diff --git a/html/Admin/FormTools/Create.html b/html/Admin/FormTools/Create.html
index cbe5b7a..062b839 100644
--- a/html/Admin/FormTools/Create.html
+++ b/html/Admin/FormTools/Create.html
@@ -3,24 +3,12 @@
 <& /Elements/ListActions, actions => \@results &>
 
 <form action="<%RT->Config->Get('WebPath')%>/Admin/FormTools/Create.html" name="CreateFormTools" method="post" enctype="multipart/form-data" class="mx-auto max-width-sm">
-  <div class="form-row">
-    <div class="col-3 label">
-      <&|/l&>Name</&>:
-    </div>
-    <div class="col-9 value">
-      <input name="Description" class="form-control" value="<% $ARGS{Description} // '' %>"/>
-    </div>
-  </div>
-  <div class="form-row">
-    <div class="col-3 label">
-      <span class="prev-icon-helper"><&|/l&>Queue</&>:</span>\
-      <span class="far fa-question-circle icon-helper" data-toggle="tooltip" data-placement="top" data-original-title="<% loc("Tickets will be created in this queue") %>"></span>
-    </div>
-    <div class="col-9 value">
-      <& /Elements/SelectQueue, Name => 'Queue', Default => $ARGS{Queue} &>
-    </div>
-  </div>
+  <&| /Elements/LabeledValue, Label => loc('Name') &>
+    <input name="Description" class="form-control" value="<% $ARGS{Description} // '' %>"/>
+  </&>
 
+  <& /Elements/LabeledValue, Label => loc('Queue'), LabelTooltip => loc('Tickets will be created in this queue'),
+                             RawValue => $m->scomp('/Elements/SelectQueue', Name => 'Queue', Default => $ARGS{Queue}) &>
   <div class="form-row">
     <div class="col-12">
       <& /Elements/Submit, Label => loc('Create'), Name => 'Create' &>
diff --git a/html/Admin/FormTools/Modify.html b/html/Admin/FormTools/Modify.html
index 9b23c46..82162a2 100644
--- a/html/Admin/FormTools/Modify.html
+++ b/html/Admin/FormTools/Modify.html
@@ -79,18 +79,12 @@
                     </a>
                   </div>
                   <div class="modal-body">
-                    <div class="form-row">
-                      <div class="col-3 label"><&|/l&>Label</&>:</div>
-                      <div class="col-9 value">
-                        <input name="label" type="text" class="form-control" placeholder="<% $item %>" value="" />
-                      </div>
-                    </div>
-                    <div class="form-row">
-                      <div class="col-3 label"><&|/l&>Default Value</&>:</div>
-                      <div class="col-9 value">
-                        <input name="default" type="text" class="form-control" placeholder="<% $default_values{$item} %>" value="" />
-                      </div>
-                    </div>
+                    <&| /Elements/LabeledValue, Label => loc('Label') &>
+                      <input name="label" type="text" class="form-control" placeholder="<% $item %>" value="" />
+                    </&>
+                    <&| /Elements/LabeledValue, Label => loc('Default Value') &>
+                      <input name="default" type="text" class="form-control" placeholder="<% $default_values{$item} %>" value="" />
+                    </&>
                   </div>
                   <div class="modal-footer">
                     <button type="submit" class="btn btn-primary button form-control"><% loc('Save') %></button>
@@ -126,29 +120,20 @@
                     </a>
                   </div>
                   <div class="modal-body">
-                    <div class="form-row">
-                      <div class="col-3 label"><&|/l&>Label</&>:</div>
-                      <div class="col-9 value">
-                        <input name="label" type="text" class="form-control" placeholder="<% $item %>" value="" />
-                      </div>
-                    </div>
-                    <div class="form-row">
-                      <div class="col-3 label"><&|/l&>Default Value</&>:</div>
-                      <div class="col-9 value">
-                        <input name="default" type="text" class="form-control" placeholder="<% $default_values{$item} %>" value="" />
-                      </div>
-                    </div>
-                    <div class="form-row">
-                      <div class="col-3 label"></div>
-                      <div class="col-9 value">
-                        <div class="custom-control custom-checkbox">
-                          <input class="custom-control-input" id="<% CSSClass($item) %>-validation" type="checkbox" name="show_validation" value="1" checked="checked" />
-                          <label class="custom-control-label" for="<% CSSClass($item) %>-validation">
-                            <&|/l&>Show validation</&>
-                          </label>
-                        </div>
+                    <&| /Elements/LabeledValue, Label => loc('Label') &>
+                      <input name="label" type="text" class="form-control" placeholder="<% $item %>" value="" />
+                    </&>
+                    <&| /Elements/LabeledValue, Label => loc('Default Value') &>
+                      <input name="default" type="text" class="form-control" placeholder="<% $default_values{$item} %>" value="" />
+                    </&>
+                    <&| /Elements/LabeledValue, Label => '' &>
+                      <div class="custom-control custom-checkbox">
+                        <input class="custom-control-input" id="<% CSSClass($item) %>-validation" type="checkbox" name="show_validation" value="1" checked="checked" />
+                        <label class="custom-control-label" for="<% CSSClass($item) %>-validation">
+                          <&|/l&>Show validation</&>
+                        </label>
                       </div>
-                    </div>
+                    </&>
                   </div>
                   <div class="modal-footer">
                     <button type="submit" class="btn btn-primary button form-control"><% loc('Save') %></button>
@@ -189,18 +174,12 @@
                     </a>
                   </div>
                   <div class="modal-body">
-                    <div class="form-row">
-                      <div class="col-3 label"><&|/l&>Name</&>:</div>
-                      <div class="col-9 value">
-                        <input name="name" type="text" class="form-control" value="" />
-                      </div>
-                    </div>
-                    <div class="form-row">
-                      <div class="col-3 label"><&|/l&>Value</&>:</div>
-                      <div class="col-9 value">
-                        <input name="value" type="text" class="form-control" value="" />
-                      </div>
-                    </div>
+                    <&| /Elements/LabeledValue, Label => loc('Name') &>
+                      <input name="name" type="text" class="form-control" value="" />
+                    </&>
+                    <&| /Elements/LabeledValue, Label => loc('Value') &>
+                      <input name="value" type="text" class="form-control" value="" />
+                    </&>
                   </div>
                   <div class="modal-footer">
                     <button type="submit" class="btn btn-primary button form-control"><% loc('Save') %></button>
@@ -239,37 +218,26 @@
       <div id="formtools-content-<% $form_page_id{$page_name} %>" class="tab-pane <% $form_page_id{$page_name} eq $active_context->{tab} ? 'show active' : 'fade' %>" role="tabpanel">
         <&| /Widgets/TitleBox, title => loc('Basic Info'), titleright_raw => qq{} &>
         <form>
-          <div class="form-row">
-            <div class="col-3 label"><&|/l&>Name</&>:</div>
-            <div class="col-9 value">
-              <input name="name" class="form-control" value="<% $form->{'formtools-pages'}{$page_name}{name} %>" />
-            </div>
-          </div>
-          <div class="form-row">
-            <div class="col-3 label"><&|/l&>Sort order</&>:</div>
-            <div class="col-9 value">
-              <input name="sort_order" class="form-control" value="<% $form->{'formtools-pages'}{$page_name}{sort_order} %>" />
+          <&| /Elements/LabeledValue, Label => loc('Name') &>
+            <input name="name" class="form-control" value="<% $form->{'formtools-pages'}{$page_name}{name} %>" />
+          </&>
+          <&| /Elements/LabeledValue, Label => loc('Sort Order') &>
+            <input name="sort_order" class="form-control" value="<% $form->{'formtools-pages'}{$page_name}{sort_order} %>" />
+          </&>
+          <&| /Elements/LabeledValue, Label => '' &>
+            <div class="custom-control custom-checkbox">
+              <input class="custom-control-input" id="<% $form_page_id{$page_name} %>-validation" type="checkbox" name="validation" value="1" <% $form->{'formtools-pages'}{$page_name}{validation} ? 'checked="checked"' : '' |n%> />
+              <label class="custom-control-label" for="<% $form_page_id{$page_name} %>-validation">
+                <&|/l&>Enable validation</&>
+              </label>
             </div>
-          </div>
-          <div class="form-row">
-            <div class="col-3 label"></div>
-            <div class="col-9 value">
-              <div class="custom-control custom-checkbox">
-                <input class="custom-control-input" id="<% $form_page_id{$page_name} %>-validation" type="checkbox" name="validation" value="1" <% $form->{'formtools-pages'}{$page_name}{validation} ? 'checked="checked"' : '' |n%> />
-                <label class="custom-control-label" for="<% $form_page_id{$page_name} %>-validation">
-                  <&|/l&>Enable validation</&>
-                </label>
-              </div>
-            </div>
-          </div>
+          </&>
+
 %         # Do not delete the last one
 %         if ( $form->{'formtools-pages'}{$page_name}{next} ) {
-          <div class="form-row">
-            <div class="col-3 label"></div>
-            <div class="col-9 value">
-              <a class="formtools-delete-page btn btn-primary button" data-page="<% $page_name %>" href="#"><% loc('Delete Page') %></a>
-            </div>
-          </div>
+          <&| /Elements/LabeledValue, Label => '' &>
+            <a class="formtools-delete-page btn btn-primary button" data-page="<% $page_name %>" href="#"><% loc('Delete Page') %></a>
+          </&>
 %         }
         </form>
         </&>
@@ -336,44 +304,29 @@
 %                       }
                         </&>
 %                     } elsif ( $item->{type} eq 'component' && $item->{comp_name} eq 'Field' ) {
-                        <div class="form-row">
-                          <div class="col-3 label"><&|/l&>Label</&>:</div>
-                          <div class="col-9 value">
-                            <input name="label" type="text" class="form-control" placeholder="<% $item->{arguments}{name} %>" value="<% $item->{arguments}{label} // ''  %>" />
-                          </div>
-                        </div>
-                        <div class="form-row">
-                          <div class="col-3 label"><&|/l&>Default Value</&>:</div>
-                          <div class="col-9 value">
-                            <input name="default" type="text" class="form-control" placeholder="<% $default_values{$item->{arguments}{name}} %>" value="<% $item->{arguments}{default} // '' %>" />
-                          </div>
-                        </div>
+                        <&| /Elements/LabeledValue, Label => loc('Label') &>
+                          <input name="label" type="text" class="form-control" placeholder="<% $item->{arguments}{name} %>" value="<% $item->{arguments}{label} // ''  %>" />
+                        </&>
+                        <&| /Elements/LabeledValue, Label => loc('Default Value') &>
+                          <input name="default" type="text" class="form-control" placeholder="<% $default_values{$item->{arguments}{name}} %>" value="<% $item->{arguments}{default} // '' %>" />
+                        </&>
 %                       if ( !RT::Extension::FormTools::is_core_field($item->{arguments}{name}) ) {
-                        <div class="form-row">
-                          <div class="col-3 label"></div>
-                          <div class="col-9 value">
-                            <div class="custom-control custom-checkbox">
-                              <input class="custom-control-input" id="formtools-element-<% $form_page_id{$page_name} %>-<% $i %>-validation" type="checkbox" name="show_validation" value="1" <% $item->{arguments}{show_validation} ? q{checked="checked"} : '' |n %> />
-                              <label class="custom-control-label" for="formtools-element-<% $form_page_id{$page_name} %>-<% $i %>-validation">
-                                <&|/l&>Show validation</&>
-                              </label>
-                            </div>
+                        <&| /Elements/LabeledValue, Label => '' &>
+                          <div class="custom-control custom-checkbox">
+                            <input class="custom-control-input" id="formtools-element-<% $form_page_id{$page_name} %>-<% $i %>-validation" type="checkbox" name="show_validation" value="1" <% $item->{arguments}{show_validation} ? q{checked="checked"} : '' |n %> />
+                            <label class="custom-control-label" for="formtools-element-<% $form_page_id{$page_name} %>-<% $i %>-validation">
+                              <&|/l&>Show validation</&>
+                            </label>
                           </div>
-                        </div>
+                        </&>
 %                       }
 %                     } elsif ( $item->{type} eq 'hidden' ) {
-                        <div class="form-row">
-                          <div class="col-3 label"><&|/l&>Name</&>:</div>
-                          <div class="col-9 value">
-                            <input name="name" type="text" class="form-control" value="<% $item->{'input-name'} // ''  %>" />
-                          </div>
-                        </div>
-                        <div class="form-row">
-                          <div class="col-3 label"><&|/l&>Value</&>:</div>
-                          <div class="col-9 value">
-                            <input name="value" type="text" class="form-control" value="<% $item->{'input-value'} // '' %>" />
-                          </div>
-                        </div>
+                        <&| /Elements/LabeledValue, Label => loc('Name') &>
+                          <input name="name" type="text" class="form-control" value="<% $item->{'input-name'} // ''  %>" />
+                        </&>
+                        <&| /Elements/LabeledValue, Label => loc('Value') &>
+                          <input name="value" type="text" class="form-control" value="<% $item->{'input-value'} // '' %>" />
+                        </&>
 %                     }
                       </div>
                       <div class="modal-footer">

commit fb77f2726982b6f26f453c2f3ffcb5a3ebcf208c
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Fri Oct 6 13:11:44 2023 -0400

    Support to enable/disable show_validation for custom fields on modify page

diff --git a/html/Admin/FormTools/Modify.html b/html/Admin/FormTools/Modify.html
index b9c9248..9b23c46 100644
--- a/html/Admin/FormTools/Modify.html
+++ b/html/Admin/FormTools/Modify.html
@@ -138,6 +138,17 @@
                         <input name="default" type="text" class="form-control" placeholder="<% $default_values{$item} %>" value="" />
                       </div>
                     </div>
+                    <div class="form-row">
+                      <div class="col-3 label"></div>
+                      <div class="col-9 value">
+                        <div class="custom-control custom-checkbox">
+                          <input class="custom-control-input" id="<% CSSClass($item) %>-validation" type="checkbox" name="show_validation" value="1" checked="checked" />
+                          <label class="custom-control-label" for="<% CSSClass($item) %>-validation">
+                            <&|/l&>Show validation</&>
+                          </label>
+                        </div>
+                      </div>
+                    </div>
                   </div>
                   <div class="modal-footer">
                     <button type="submit" class="btn btn-primary button form-control"><% loc('Save') %></button>
@@ -337,6 +348,19 @@
                             <input name="default" type="text" class="form-control" placeholder="<% $default_values{$item->{arguments}{name}} %>" value="<% $item->{arguments}{default} // '' %>" />
                           </div>
                         </div>
+%                       if ( !RT::Extension::FormTools::is_core_field($item->{arguments}{name}) ) {
+                        <div class="form-row">
+                          <div class="col-3 label"></div>
+                          <div class="col-9 value">
+                            <div class="custom-control custom-checkbox">
+                              <input class="custom-control-input" id="formtools-element-<% $form_page_id{$page_name} %>-<% $i %>-validation" type="checkbox" name="show_validation" value="1" <% $item->{arguments}{show_validation} ? q{checked="checked"} : '' |n %> />
+                              <label class="custom-control-label" for="formtools-element-<% $form_page_id{$page_name} %>-<% $i %>-validation">
+                                <&|/l&>Show validation</&>
+                              </label>
+                            </div>
+                          </div>
+                        </div>
+%                       }
 %                     } elsif ( $item->{type} eq 'hidden' ) {
                         <div class="form-row">
                           <div class="col-3 label"><&|/l&>Name</&>:</div>
diff --git a/static/js/rt-extension-formtools.js b/static/js/rt-extension-formtools.js
index edd3929..d8055b7 100644
--- a/static/js/rt-extension-formtools.js
+++ b/static/js/rt-extension-formtools.js
@@ -110,6 +110,16 @@ formTools = {
             else {
                 delete value.arguments.default;
             }
+
+            const validation = form.find(':input[name=show_validation]');
+            if ( validation.length ) {
+                if ( validation.is(':checked') ) {
+                    value.arguments.show_validation = 1;
+                }
+                else {
+                    delete value.arguments.show_validation;
+                }
+            }
         }
         else if ( value.type === 'hidden' ) {
             value['input-name'] = form.find(':input[name=name]').val();

commit a05114f948884441090591afa516417aec4e3f28
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Fri Oct 6 07:29:25 2023 -0400

    Switch to a shorter page id
    
    Previously its length was 36, here we change to 8 to be more user
    friendly. This might cause conflicts, but it should be really rare in
    real world, and we can still make sure no conflicts happen in the same
    form.

diff --git a/META.yml b/META.yml
index 3cfe80b..9f86295 100644
--- a/META.yml
+++ b/META.yml
@@ -21,7 +21,6 @@ no_index:
     - inc
     - static
 requires:
-  UUID::Tiny: 0
   perl: 5.10.1
 resources:
   license: http://opensource.org/licenses/gpl-license.php
diff --git a/Makefile.PL b/Makefile.PL
index 0ae9903..ed71e84 100644
--- a/Makefile.PL
+++ b/Makefile.PL
@@ -1,7 +1,6 @@
 use inc::Module::Install;
 RTx('RT-Extension-FormTools');
 requires_rt('5.0.0');
-requires('UUID::Tiny');
 
 repository('https://github.com/bestpractical/rt-extension-formtools');
 
diff --git a/html/Admin/FormTools/Modify.html b/html/Admin/FormTools/Modify.html
index 62e1311..b9c9248 100644
--- a/html/Admin/FormTools/Modify.html
+++ b/html/Admin/FormTools/Modify.html
@@ -407,8 +407,6 @@ unless ( $ok ) {
 
 my $form = $form_attribute->Content;
 
-use UUID::Tiny 'create_uuid_as_string';
-
 my @results;
 
 if ( $AddPage ) {
@@ -417,7 +415,7 @@ if ( $AddPage ) {
 
     my @orders = map { $form->{'formtools-pages'}{$_}{sort_order} } keys %{$form->{'formtools-pages'}};
 
-    my $new_page = create_uuid_as_string();
+    my $new_page = RT::Extension::FormTools->GeneratePageId($form);
     $form->{'formtools-pages'}{$new_page} = {
         name       => 'Page New',
         sort_order => ( $orders[-2] || 0 ) + 1,
@@ -518,8 +516,8 @@ my %other_components = (
 
 
 $form->{'formtools-pages'} ||= {
-    create_uuid_as_string() => { sort_order => 1, name => 'Page 1' },
-    create_uuid_as_string() => {
+    RT::Extension::FormTools->GeneratePageId() => { sort_order => 1, name => 'Page 1' },
+    RT::Extension::FormTools->GeneratePageId() => {
         sort_order => 999,
         name       => 'Result',
         content    => [
diff --git a/lib/RT/Extension/FormTools.pm b/lib/RT/Extension/FormTools.pm
index 56cfbc2..7a85f9a 100644
--- a/lib/RT/Extension/FormTools.pm
+++ b/lib/RT/Extension/FormTools.pm
@@ -8,6 +8,45 @@ our $VERSION = '0.53';
 RT->AddStyleSheets('rt-extension-formtools.css');
 RT->AddJavaScript('rt-extension-formtools.js');
 
+use Time::HiRes 'time';
+use Digest::SHA 'sha1_hex';
+
+# page ids are based on current time, keep 100 recent ids in case CPU is really fast
+my @recent_page_ids;
+
+sub GeneratePageId {
+    shift if ( $_[0] // '' ) eq __PACKAGE__;
+    my $form = shift;
+    my %current;
+    if ($form) {
+        %current = map { $_ => 1 } keys %{ $form->{'formtools-pages'} };
+    }
+
+    my %skip = (
+        map { $_ => 1 } @recent_page_ids,
+        $form && $form->{'formtools-pages'} ? keys %{ $form->{'formtools-pages'} } : ()
+    );
+
+    my $page_id = _GeneratePageId();
+
+    for ( 1 .. 100 ) {
+        if ( $skip{$page_id} ) {
+            $page_id = _GeneratePageId();
+        }
+        else {
+            push @recent_page_ids, $page_id;
+            shift @recent_page_ids while @recent_page_ids > 100;
+            return $page_id;
+        }
+    }
+    RT->Logger->error("Could not generate a new page id");
+    return;
+}
+
+sub _GeneratePageId {
+    return substr( sha1_hex( time . int rand 10000 ), 0, 8 );
+}
+
 =head1 NAME
 
 RT-Extension-FormTools - Help write multi-page ticket creation wizards

commit fb9708c226cf0c435d3cc61165acf8af2da676d5
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Thu Oct 5 17:39:00 2023 -0400

    Tweak badge words to be a bit more clear

diff --git a/html/Admin/FormTools/Modify.html b/html/Admin/FormTools/Modify.html
index d1f0f0a..62e1311 100644
--- a/html/Admin/FormTools/Modify.html
+++ b/html/Admin/FormTools/Modify.html
@@ -59,7 +59,7 @@
         <div id="formtools-element-<% $item %>" class="formtools-element" draggable="true" ondragstart="formTools.dragstart(event);" ondragend="formTools.dragend(event);" data-value="<% JSON({ type => 'component', comp_name => 'Field', arguments => { name => $item } }) %>">
           <p class="formtools-element-placeholder m-1 p-2 border rounded"><% loc('Put it here') %></p>
           <p class="m-1 p-2 border rounded">
-            <span class="badge badge-primary">component</span>
+            <span class="badge badge-primary"><% loc('Field') %></span>
             <% $item %>
             <a href="#" class="edit" data-toggle="modal" data-target="#formtools-element-modal">
               <span class="fas fa-pencil-alt" alt="<% loc('Edit') %>" data-toggle="tooltip" data-placement="top" data-original-title="<% loc('Edit') %>"></span>
@@ -106,7 +106,7 @@
         <div id="formtools-element-<% $item %>" class="formtools-element" draggable="true" ondragstart="formTools.dragstart(event);" ondragend="formTools.dragend(event);" data-value="<% JSON({ type => 'component', comp_name => 'Field', arguments => { name => $item } }) %>">
           <p class="formtools-element-placeholder m-1 p-2 border rounded"><% loc('Put it here') %></p>
           <p class="m-1 p-2 border rounded">
-            <span class="badge badge-primary">component</span>
+            <span class="badge badge-primary"><% loc('Field') %></span>
             <% $item %>
             <a href="#" class="edit" data-toggle="modal" data-target="#formtools-element-modal">
               <span class="fas fa-pencil-alt" alt="<% loc('Edit') %>" data-toggle="tooltip" data-placement="top" data-original-title="<% loc('Edit') %>"></span>
@@ -154,7 +154,7 @@
         <div id="formtools-element-<% $item %>" class="formtools-element" draggable="true" ondragstart="formTools.dragstart(event);" ondragend="formTools.dragend(event);" data-value="<% JSON($other_components{$item}) %>">
           <p class="formtools-element-placeholder m-1 p-2 border rounded"><% loc('Put it here') %></p>
           <p class="m-1 p-2 border rounded">
-            <span class="badge badge-primary"><% $other_components{$item}{type} %></span>
+            <span class="badge badge-primary"><% $other_components{$item}{type} eq 'hidden' ? loc('Hidden') : loc('Component') %></span>
             <span class="content"><% $item %></span>
 %         if ( $item eq 'Hidden' ) {
             <a href="#" class="edit" data-toggle="modal" data-target="#formtools-element-modal">
@@ -270,7 +270,17 @@
             <div id="formtools-element-<% $form_page_id{$page_name} %>-<% $i %>" class="formtools-element" draggable="true" ondragstart="formTools.dragstart(event);" ondragend="formTools.dragend(event);" ondragenter="formTools.dragenter(event)" ondragleave="formTools.dragleave(event)" data-value="<% JSON($item) %>">
               <p class="formtools-element-placeholder m-1 p-2 border rounded"><% loc('Put it here') %></p>
               <p class="m-1 p-2 border rounded">
-                <span class="badge badge-primary"><% $item->{type} eq 'raw_html' ? uc($item->{wrapper} || 'HTML') : $item->{type} %></span>
+                <span class="badge badge-primary">
+%                 if ( $item->{type} eq 'raw_html' ) {
+                    <% uc($item->{wrapper} || 'HTML') %>
+%                 } elsif ( ( $item->{comp_name} // '' ) eq 'ShowChoices' ) {
+                    <% loc('Component') %>
+%                 } elsif ( $item->{type} eq 'hidden' ) {
+                    <% loc('Hidden') %>
+%                 } else {
+                    <% loc('Field') %>
+%                 }
+                </span>
                 <span class="content">
 %               if ( $item->{type} eq 'raw_html' ) {
 %                 if ( $item->{wrapper} ) {

commit ad058adc128aa77957503d7af08636d82407940c
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Thu Oct 5 16:44:58 2023 -0400

    Make html elements be easier to review and modify
    
    For h1, h2, h3, and p, users can just input real content, and we can add
    the html wrapper automatically. Elements h4, h5, and h6 are dropped
    because they are not used so often.
    
    Here we also add a new no-wrapper HTML element, where user can input
    anything they want.

diff --git a/html/Admin/FormTools/Modify.html b/html/Admin/FormTools/Modify.html
index 83217ea..d1f0f0a 100644
--- a/html/Admin/FormTools/Modify.html
+++ b/html/Admin/FormTools/Modify.html
@@ -11,7 +11,7 @@
         <div id="formtools-element-<% $item %>" class="formtools-element" draggable="true" ondragstart="formTools.dragstart(event);" ondragend="formTools.dragend(event);" data-value="<% JSON({ type => 'raw_html', html => "<$item>test</$item>"}) %>">
           <p class="formtools-element-placeholder m-1 p-2 border rounded"><% loc('Put it here') %></p>
           <p class="m-1 p-2 border rounded">
-            <span class="badge badge-primary">raw_html</span>
+            <span class="badge badge-primary"><% $item eq 'html' ? 'HTML' : uc($item) %></span>
             <span class="content"><% loc('[_1] Element', uc($item)) %></span>
 %           if ( $item ne 'hr' ) {
             <a href="#" class="edit" data-toggle="modal" data-target="#formtools-element-modal">
@@ -34,7 +34,15 @@
                     </a>
                   </div>
                   <div class="modal-body">
-                    <textarea name="html" class="form-control"><% "<$item>Text</$item>" %></textarea>
+                    <&| /Elements/LabeledValue, Label => loc("Content") &>
+%                   if ( $item =~ /^h\d/ ) {
+                      <input name="content" class="form-control" data-wrapper="<% $item %>" />
+%                   } elsif ( $item eq 'p' ) {
+                      <textarea name="content" class="form-control" data-wrapper="<% $item %>"></textarea>
+%                   } elsif ( $item eq 'html' ) {
+                      <textarea name="content" class="form-control"></textarea>
+%                   }
+                    </&>
                   </div>
                   <div class="modal-footer">
                     <button type="submit" class="btn btn-primary button form-control"><% loc('Save') %></button>
@@ -262,10 +270,14 @@
             <div id="formtools-element-<% $form_page_id{$page_name} %>-<% $i %>" class="formtools-element" draggable="true" ondragstart="formTools.dragstart(event);" ondragend="formTools.dragend(event);" ondragenter="formTools.dragenter(event)" ondragleave="formTools.dragleave(event)" data-value="<% JSON($item) %>">
               <p class="formtools-element-placeholder m-1 p-2 border rounded"><% loc('Put it here') %></p>
               <p class="m-1 p-2 border rounded">
-                <span class="badge badge-primary"><% $item->{type} %></span>
+                <span class="badge badge-primary"><% $item->{type} eq 'raw_html' ? uc($item->{wrapper} || 'HTML') : $item->{type} %></span>
                 <span class="content">
 %               if ( $item->{type} eq 'raw_html' ) {
-                  <% length $item->{html} > 40 ? substr($item->{html}, 0, 40) . '...' : $item->{html} %>
+%                 if ( $item->{wrapper} ) {
+                    <% length $item->{content} > 40 ? substr($item->{content}, 0, 40) . '...' : $item->{content} %>
+%                 } else {
+                    <% length $item->{html} > 40 ? substr($item->{html}, 0, 40) . '...' : $item->{html} %>
+%                 }
 %               } elsif ( $item->{type} eq 'hidden' ) {
                   <% $item->{'input-name'} %>: <% $item->{'input-value'} %>
 %               } else {
@@ -293,7 +305,15 @@
                       </div>
                       <div class="modal-body">
 %                     if ( $item->{type} eq 'raw_html' ) {
-                        <textarea name="html" class="form-control"><% $item->{html} %></textarea>
+                        <&| /Elements/LabeledValue, Label => loc("Content") &>
+%                       if ( ($item->{wrapper} // '') =~ /^h\d/i ) {
+                          <input name="content" class="form-control" data-wrapper="<% $item->{wrapper} %>" value="<% $item->{content} %>"/>
+%                       } elsif ( lc ($item->{wrapper} // '') eq 'p' ) {
+                          <textarea name="content" class="form-control" data-wrapper="<% $item->{wrapper} %>"><% $item->{content} %></textarea>
+%                       } elsif ( $item eq 'html' ) {
+                          <textarea name="content" class="form-control"><% $item->{html} %></textarea>
+%                       }
+                        </&>
 %                     } elsif ( $item->{type} eq 'component' && $item->{comp_name} eq 'Field' ) {
                         <div class="form-row">
                           <div class="col-3 label"><&|/l&>Label</&>:</div>
@@ -462,7 +482,7 @@ my $title = loc("Modify form [_1]", $form_attribute->Description);
 
 my $nav_type = 'pill'; # 'tab' or 'pill'
 
-my @html_components = qw( h1 h2 h3 h4 h5 h6 hr p );
+my @html_components = qw( h1 h2 h3 hr p html );
 my @core_components = qw( Requestors Owner Subject Content );
 
 my $queue = RT::Queue->new($session{'CurrentUser'});
diff --git a/static/js/rt-extension-formtools.js b/static/js/rt-extension-formtools.js
index 905714b..edd3929 100644
--- a/static/js/rt-extension-formtools.js
+++ b/static/js/rt-extension-formtools.js
@@ -80,8 +80,18 @@ formTools = {
         const value = element.data('value');
 
         if ( value.type === 'raw_html' ) {
-            value.html = form.find(':input[name=html]').val();
-            element.find('span.content').text(value.html.length > 40 ? value.html.substr(0, 40) + '...' : value.html);
+            const input = form.find(':input[name=content]');
+            const content = input.val();
+            const wrapper = input.data('wrapper');
+            if ( wrapper ) {
+                value.content = content;
+                value.wrapper = wrapper;
+                value.html = '<' + wrapper + '>' + content + '</' + wrapper + '>';
+            }
+            else {
+                value.html = content;
+            }
+            element.find('span.content').text(content.length > 40 ? content.substr(0, 40) + '...' : content);
         }
         else if ( value.type === 'component' && value.comp_name === 'Field' ) {
             const label = form.find(':input[name=label]').val();

commit d5b67dfec6666e4f0038756995c8a13e02e322c7
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Thu Oct 5 15:50:12 2023 -0400

    Fix initial pages to generate uuid as keys
    
    Not adding "()" made "create_uuid_as_string" a plain string :/

diff --git a/html/Admin/FormTools/Modify.html b/html/Admin/FormTools/Modify.html
index 338b55a..83217ea 100644
--- a/html/Admin/FormTools/Modify.html
+++ b/html/Admin/FormTools/Modify.html
@@ -488,8 +488,8 @@ my %other_components = (
 
 
 $form->{'formtools-pages'} ||= {
-    create_uuid_as_string => { sort_order => 1, name => 'Page 1' },
-    create_uuid_as_string => {
+    create_uuid_as_string() => { sort_order => 1, name => 'Page 1' },
+    create_uuid_as_string() => {
         sort_order => 999,
         name       => 'Result',
         content    => [

commit 8b09c335da5af3d0360a727be64bf2cd129c6dbe
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Thu Oct 5 09:47:07 2023 -0400

    No need to add the OR("|") inside "[]" in regexes
    
    "|" doesn't have a special meaning inside "[]"

diff --git a/README b/README
index 2e3d8ff..2509a8f 100644
--- a/README
+++ b/README
@@ -89,6 +89,11 @@ FUNCTIONS
   has_value value
     Returns true if the value is defined and non-empty.
 
+  LoadFormIcon($current_user, $form_id)
+    Loads the form icon attribute associated with the passed form id.
+
+    Returns a tuple of attribute object or false, and a message.
+
 AUTHOR
     Best Practical Solutions, LLC <modules at bestpractical.com>
 
diff --git a/html/Forms/dhandler b/html/Forms/dhandler
index 9854928..45da273 100644
--- a/html/Forms/dhandler
+++ b/html/Forms/dhandler
@@ -28,7 +28,7 @@ foreach my $element ( @{$form_config->{'formtools-pages'}{$page}{'content'}} ) {
 my $path = $m->dhandler_arg;
 my ($form_name, $page_name);
 
-if ( $path =~ /^([\w|\s]+)\/([-\w]+)$/ ) {
+if ( $path =~ /^([\w\s]+)\/([-\w]+)$/ ) {
     $form_name = $1;
     $page_name = $2;
 }
@@ -37,7 +37,7 @@ else {
 }
 
 # Limit to names to letters, numbers, underscore, spaces
-unless ( $form_name =~ /^[\w|\s]+$/ ) {
+unless ( $form_name =~ /^[\w\s]+$/ ) {
     RT->Logger->error("FormTools called with invalid form name: $form_name");
     Abort('Invalid form name');
 }

commit 03ce4ee935190e379769071c5542fbabd72327cb
Merge: d32cbe0 92009cb
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Thu Oct 5 09:39:45 2023 -0400

    Merge branch 'form-home-icon-upload' into dynamic-forms-from-config

diff --cc static/css/rt-extension-formtools.css
index f9288f3,50224f5..b5d719f
--- a/static/css/rt-extension-formtools.css
+++ b/static/css/rt-extension-formtools.css
@@@ -3,43 -3,15 +3,56 @@@ div .formtools-content 
      min-height: 400px;
  }
  
 +.formtools-form-pages .badge {
 +    background-color: #4868b3;
 +}
 +
 +.formtools-form-pages .remove {
 +    position: absolute;
 +    right: 2.5em;
 +    color: #5C6273;
 +}
 +
 +.formtools-component-menu .remove {
 +    display: none;
 +}
 +
 +.formtools-form-pages .edit {
 +    position: absolute;
 +    right: 4em;
 +    color: #5C6273;
 +}
 +
 +.formtools-component-menu .edit {
 +    display: none;
 +}
 +
 +.formtools-component-menu .badge {
 +    display: none;
 +}
 +
 +.formtools-element-placeholder {
 +    border: 2px dotted #ffc107 !important;
 +    display: none;
 +}
 +
 +.formtools-element-placeholder.active {
 +    display: block;
 +}
 +
 +.formtools-element.current p {
 +    opacity: 0.7;
- }
++}
++
+ p.formtools-form-graphic {
+     font-weight: bold;
+     font-size: 1.5rem;
+ }
+ 
+ div .formtools-form-list {
+     max-width: 1000px;
+ }
+ 
+ div .formtools-admin-description {
+     max-width: 1000px;
+ }

commit 92009cbf35858819e434ae99d2bd7fc5f4bf8ec0
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Wed Oct 4 15:55:50 2023 -0400

    Add icon upload and display for forms

diff --git a/html/Admin/FormTools/Describe.html b/html/Admin/FormTools/Describe.html
index a29ae35..a4a1635 100644
--- a/html/Admin/FormTools/Describe.html
+++ b/html/Admin/FormTools/Describe.html
@@ -2,16 +2,28 @@
 <& /Elements/Tabs &>
 <& /Elements/ListActions, actions => \@results &>
 
-<&| /Widgets/TitleBox, title => '' &>
+<&| /Widgets/TitleBox, title => '', class => 'formtools-admin-description ml-auto mr-auto' &>
   <p>Forms are shown to users on a page in the RT Self Service interface. Below you can manage how the form details will appear to end users.</p>
   <form name="EditFormDescription" action="<%RT->Config->Get('WebPath')%>/Admin/FormTools/Describe.html" method="post" enctype="multipart/form-data">
   <input type="hidden" class="hidden" name="id" value="<% $id %>" />
   <&| /Widgets/TitleBox, title => loc('Icon') &>
-    <p>Form to upload an icon</p>
+% if ( $current_form_icon ) {
+<& /Elements/Forms/ShowFormIcon, FormID => $id &>
+% } else {
+      <p>Upload an icon to be shown to users on the Forms page or select a color for the background of graphical box users can click.</p>
+% }
+
+      <div class="row">
+        <div class="col ml-5 mr-5">
+          <div class="custom-file">
+            <input type="file" name="FormUploadIcon" class="custom-file-input" id="formtools-upload-icon" />
+            <label class="custom-file-label" for="formtools-upload-icon"><&|/l&>Upload an icon for this form</&></label>
+          </div>
+        </div>
+      </div>
   </&>
   <&| /Widgets/TitleBox, title => loc('Description') &>
     <p>Describe what the form should be used for and include instructions to help users pick the correct form.</p>
-    <p>An HTML edit box for instructions</p>
       <div class="form-row">
         <div class="col-12">
           <input type="hidden" class="hidden" name="FormDescriptionType" value="text/html" />
@@ -27,6 +39,7 @@
 </div>
 </form>
 <%init>
+use Digest::MD5 'md5_hex';
 
 # Handle id getting submitted twice and becoming an array
 my @id = ( ref $id eq 'ARRAY' ) ? @{$id} : ($id);
@@ -61,6 +74,35 @@ if ( $ARGS{'SubmitDescription'} ) {
     }
 }
 
+my ($current_form_icon) = RT::Extension::FormTools::LoadFormIcon( $session{'CurrentUser'}, $form_attribute->Id );
+
+if ( my $file_hash = _UploadedFile( 'FormUploadIcon' ) ) {
+
+    my ($form_icon, $msg) = RT::Extension::FormTools::LoadFormIcon( $session{'CurrentUser'}, $form_attribute->Id );
+
+    if ( $form_icon ) {
+        # Delete the existing icon
+        my ( $del_ok, $del_msg ) = $form_icon->Delete;
+
+        RT->Logger->error("Unable to delete icon attribute id $ok, $del_msg") unless $del_ok;
+    }
+
+    my $new_form_icon = RT::Attribute->new( $session{CurrentUser} );
+    ( $ok, $msg ) = $new_form_icon->Create(
+        Name        => "FormTools Icon",
+        Description => "Icon for " . $form_attribute->Description,
+        Object      => $form_attribute,
+        Content     => {
+            type => $file_hash->{ContentType},
+            data => $file_hash->{LargeContent},
+            hash => md5_hex($file_hash->{LargeContent}),
+        },
+    );
+
+    push @results, loc("Unable to set form icon") unless $ok;
+    RT->Logger->error("Unable to set form icon: $msg") unless $ok;
+}
+
 MaybeRedirectForResults(
     Actions   => \@results,
     Arguments => { id => $id, FormDescription => $ARGS{'FormDescription'} },
diff --git a/html/Elements/Forms/ListForms b/html/Elements/Forms/ListForms
index 17bed2f..f14f482 100644
--- a/html/Elements/Forms/ListForms
+++ b/html/Elements/Forms/ListForms
@@ -6,7 +6,7 @@
 <div class="row mt-3 mb-3">
   <div class="col-4">
     <a href="<% RT->Config->Get('WebPath') %>/SelfService/Forms/<% $form_attribute->Description %>">
-      <% $m->scomp('/Elements/Forms/ShowFormGraphic', FormName => $form_attribute->Description, ItemNumber => $item_number) |n %>
+      <% $m->scomp('/Elements/Forms/ShowFormGraphic', FormObj => $form_attribute, ItemNumber => $item_number) |n %>
     </a>
   </div>
   <div class="col">
diff --git a/html/Elements/Forms/ShowFormGraphic b/html/Elements/Forms/ShowFormGraphic
index 2cc953e..fd14ab8 100644
--- a/html/Elements/Forms/ShowFormGraphic
+++ b/html/Elements/Forms/ShowFormGraphic
@@ -1,12 +1,21 @@
 <div class="formtools-form-graphic w-50 mt-2 mb-2 ml-auto mr-auto">
+% if ( $has_icon ) {
+<& /Elements/Forms/ShowFormIcon, FormID => $FormObj->Id &>
+% } else {
   <div class="rounded pt-5 pb-5" style="background-color: <%$color%>;">
     <p class="text-center formtools-form-graphic">
-      <% $FormName %>
+      <% $FormObj->Description %>
     </p>
   </div>
+% }
 </div>
 <%init>
 
+unless ( $FormObj and $FormObj->Id ) {
+    RT->Logger->error("No valid form attribute object passed");
+    return;
+}
+
 my $color_list = [
     "#1f77b4", "#ff7f0e", "#2ca02c", "#d62728",
     "#9467bd", "#8c564b", "#e377c2", "#7f7f7f"
@@ -14,8 +23,16 @@ my $color_list = [
 
 my $color = $color_list->[ $ItemNumber % 8 ];
 
+my $has_icon = 0;
+my $form_icon = RT::Attribute->new( RT->SystemUser );
+my ($ok, $msg) = $form_icon->LoadByCols( ObjectType => 'RT::Attribute', ObjectId => $FormObj->Id );
+
+if ( $ok and $form_icon->Id ) {
+    $has_icon = 1;
+}
+
 </%init>
 <%args>
-$FormName => "Form"
+$FormObj
 $ItemNumber => 1
 </%args>
diff --git a/html/Elements/Forms/ShowFormIcon b/html/Elements/Forms/ShowFormIcon
new file mode 100644
index 0000000..cdbc216
--- /dev/null
+++ b/html/Elements/Forms/ShowFormIcon
@@ -0,0 +1,28 @@
+<div id="formtools-icon-<% $FormID %>" class="">
+% if ($form_icon) {
+<img
+    class="d-block mx-auto",
+    width="100px", height="100px",
+    src="<% RT->Config->Get('WebPath') %>/NoAuth/Helpers/FormLogo/<% $form_icon->Id %>"
+    alt="<%$ARGS{'LogoAltText'}%>" />
+% }
+</div>
+<%init>
+
+my ($form_icon, $msg) = RT::Extension::FormTools::LoadFormIcon( $session{'CurrentUser'}, $FormID );
+
+if ( $form_icon ) {
+    my $content = $form_icon->Content;
+    undef $form_icon
+        unless ref $content eq 'HASH'
+           and defined $content->{'data'};
+}
+else {
+    RT->Logger->error("Unable to load logo for form id $FormID");
+}
+
+</%init>
+<%args>
+$LogoAltText => ''
+$FormID => undef
+</%args>
diff --git a/html/NoAuth/Helpers/FormLogo/dhandler b/html/NoAuth/Helpers/FormLogo/dhandler
new file mode 100644
index 0000000..f2c33e3
--- /dev/null
+++ b/html/NoAuth/Helpers/FormLogo/dhandler
@@ -0,0 +1,17 @@
+<%init>
+my $icon_id = $m->dhandler_arg;
+
+my $form_icon = RT::Attribute->new( RT->SystemUser );
+my ($ok, $msg) = $form_icon->Load($icon_id);
+
+if ( $ok ) {
+    RT::Interface::Web::StaticFileHeaders();
+    my $content = $form_icon->Content;
+    $r->content_type( $content->{type} );
+    $m->out( $content->{data} );
+}
+else {
+    # 404
+    return $m->decline;
+}
+</%init>
diff --git a/lib/RT/Extension/FormTools.pm b/lib/RT/Extension/FormTools.pm
index c1dbd38..56cfbc2 100644
--- a/lib/RT/Extension/FormTools.pm
+++ b/lib/RT/Extension/FormTools.pm
@@ -213,6 +213,33 @@ sub has_value {
     return (0, "You must provide a value for this field");
 }
 
+=head2 LoadFormIcon($current_user, $form_id)
+
+Loads the form icon attribute associated with the passed form id.
+
+Returns a tuple of attribute object or false, and a message.
+
+=cut
+
+sub LoadFormIcon {
+    my $current_user = shift;
+    my $form_id = shift;
+
+    my $form_icon = RT::Attribute->new( $current_user );
+    my ( $ok, $msg ) = $form_icon->LoadByCols(
+        Name => 'FormTools Icon',
+        ObjectType => 'RT::Attribute',
+        ObjectId => $form_id );
+
+    if ( $ok ) {
+        return ( $form_icon, $msg );
+    }
+    else {
+        RT->Logger->error("Unable to load icon: $msg");
+        return ( 0, $msg );
+    }
+}
+
 =head1 AUTHOR
 
 Best Practical Solutions, LLC E<lt>modules at bestpractical.comE<gt>
diff --git a/static/css/rt-extension-formtools.css b/static/css/rt-extension-formtools.css
index 1108657..50224f5 100644
--- a/static/css/rt-extension-formtools.css
+++ b/static/css/rt-extension-formtools.css
@@ -11,3 +11,7 @@ p.formtools-form-graphic {
 div .formtools-form-list {
     max-width: 1000px;
 }
+
+div .formtools-admin-description {
+    max-width: 1000px;
+}

commit 47b3027da90b87b80e09e8af4ec9bb5dbb1fcf4f
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Mon Oct 2 16:49:13 2023 -0400

    Improve spacing and layout for Forms page

diff --git a/html/Elements/Forms/ListForms b/html/Elements/Forms/ListForms
index 73185f1..17bed2f 100644
--- a/html/Elements/Forms/ListForms
+++ b/html/Elements/Forms/ListForms
@@ -1,16 +1,18 @@
-<&|/Widgets/TitleBox, title => '', class => 'fullwidth' &>
+<&|/Widgets/TitleBox, title => '', class => 'fullwidth formtools-form-list ml-auto mr-auto' &>
 <div class="container">
 % my $item_number = 1;
 % while ( my $form_attribute = $forms->Next ) {
 %    my $form = $form_attribute->Content;
-<div class="row border border-primary">
+<div class="row mt-3 mb-3">
   <div class="col-4">
     <a href="<% RT->Config->Get('WebPath') %>/SelfService/Forms/<% $form_attribute->Description %>">
       <% $m->scomp('/Elements/Forms/ShowFormGraphic', FormName => $form_attribute->Description, ItemNumber => $item_number) |n %>
     </a>
   </div>
   <div class="col">
-    <% $form->{'form-description'} |n %>
+    <div class="formtools-form-text w-100 mt-2 mb-2 mr-auto ml-auto">
+      <% $form->{'form-description'} |n %>
+    </div>
   </div>
 </div>
 % $item_number++;
diff --git a/html/Elements/Forms/ShowFormGraphic b/html/Elements/Forms/ShowFormGraphic
index 37679ea..2cc953e 100644
--- a/html/Elements/Forms/ShowFormGraphic
+++ b/html/Elements/Forms/ShowFormGraphic
@@ -1,6 +1,6 @@
-<div class="formtools-form-graphic w-50 mt-2 mb-2">
+<div class="formtools-form-graphic w-50 mt-2 mb-2 ml-auto mr-auto">
   <div class="rounded pt-5 pb-5" style="background-color: <%$color%>;">
-    <p class="text-center" style="font-color: white; font-weight: bold; font-size: 2rem;">
+    <p class="text-center formtools-form-graphic">
       <% $FormName %>
     </p>
   </div>
diff --git a/static/css/rt-extension-formtools.css b/static/css/rt-extension-formtools.css
index ad60315..1108657 100644
--- a/static/css/rt-extension-formtools.css
+++ b/static/css/rt-extension-formtools.css
@@ -2,3 +2,12 @@
 div .formtools-content {
     min-height: 400px;
 }
+
+p.formtools-form-graphic {
+    font-weight: bold;
+    font-size: 1.5rem;
+}
+
+div .formtools-form-list {
+    max-width: 1000px;
+}

commit fcea9a6e057a25a0f124502da1496a0719ec1767
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Tue Sep 26 17:11:36 2023 -0400

    Customize RTs Abort to prevent permission errors from CreateTicket
    
    By default, CreateTicket calls Abort at the end if the current
    user doesn't have ShowTicket. In FormTools, we aren't showing
    the ticket at the end, we're showing the last configured form
    page. Since end users are using a form wizard to create one
    or more tickets, it's possible they don't have rights to directly
    see the newly created ticket, especially in Self Service.
    
    Add an exception to Abort so we can get the ticket object if
    the create was successful, even if the current user can't
    see the ticket.

diff --git a/html/Forms/dhandler b/html/Forms/dhandler
index c49a320..d8272ea 100644
--- a/html/Forms/dhandler
+++ b/html/Forms/dhandler
@@ -92,7 +92,16 @@ $base_path = '/SelfService' . $base_path if $SelfService;
 
 my ($ticket_obj, @results);
 if ( $create_ticket ) {
-    ($ticket_obj, @results) = CreateTicket(
+
+    # We override Abort elsewhere so we'll get $ticket_obj here even if
+    # the current user has no rights to see the newly created ticket.
+
+    # We deliberately don't pass along @results because the work
+    # behind the scenes is "magic" and we don't want the form user to
+    # see any details, like ticket ids or queues, if they are not supposed
+    # to see them.
+
+    ($ticket_obj) = CreateTicket(
         Subject => 'Ticket created from FormTools form ' . $form_name,
         Queue   => $queue_obj->Id,
         Status => 'new',
diff --git a/lib/RT/Interface/Web_Vendor.pm b/lib/RT/Interface/Web_Vendor.pm
new file mode 100644
index 0000000..16757a1
--- /dev/null
+++ b/lib/RT/Interface/Web_Vendor.pm
@@ -0,0 +1,26 @@
+package HTML::Mason::Commands;
+
+no warnings qw(redefine);
+
+# This should be the same class we are overlaying here
+my $original_abort = \&HTML::Mason::Commands::Abort;
+
+*HTML::Mason::Commands::Abort = sub {
+    my $why = shift;
+    my %args = @_;
+
+    if ( $why =~ /^No permission to view newly created ticket #(\d+)/ ) {
+        # We're showing a custom "form submitted" page, not the ticket,
+        # so we don't want to abort if the user doesn't have rights to
+        # see the ticket.
+
+        # Just return for this case so the create ticket form can get
+        # the ticket object and any actual error messages.
+
+        return;
+    }
+
+    &$original_abort( $why, %args );
+};
+
+1;

commit d32cbe0b52ce78b6e1536370cecfeadac006f3f0
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Thu Oct 5 03:10:21 2023 -0400

    Allow "-" in page name as uuid contains it

diff --git a/html/Forms/dhandler b/html/Forms/dhandler
index 790aece..5ca5fce 100644
--- a/html/Forms/dhandler
+++ b/html/Forms/dhandler
@@ -28,7 +28,7 @@ foreach my $element ( @{$form_config->{'formtools-pages'}{$page}{'content'}} ) {
 my $path = $m->dhandler_arg;
 my ($form_name, $page_name);
 
-if ( $path =~ /^([\w|\s]+)\/(\w+)$/ ) {
+if ( $path =~ /^([\w|\s]+)\/([-\w]+)$/ ) {
     $form_name = $1;
     $page_name = $2;
 }
@@ -43,7 +43,7 @@ unless ( $form_name =~ /^[\w|\s]+$/ ) {
 }
 
 if ( $page_name ) {
-    unless ( $page_name =~ /^\w+$/ ) {
+    unless ( $page_name =~ /^[-\w]+$/ ) {
         RT->Logger->error("FormTools called with invalid page name: $page_name");
         Abort('Invalid page name');
     }

commit 1aa048178a00e9dcec566f4232796cdff9c7ead5
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Wed Oct 4 17:25:24 2023 -0400

    Initial version of generally working form builder
    
    The following features have been implemented:
    
    * Add/Delete pages
    
    * Update page metadata including name, sort order, and validation.
    
    * Add/Delete/Update/Reorder fields for each page, including basic html
      elements, core fields, custom fields, and special fields(Hidden and
      ShowChoices)

diff --git a/META.yml b/META.yml
index 9f86295..3cfe80b 100644
--- a/META.yml
+++ b/META.yml
@@ -21,6 +21,7 @@ no_index:
     - inc
     - static
 requires:
+  UUID::Tiny: 0
   perl: 5.10.1
 resources:
   license: http://opensource.org/licenses/gpl-license.php
diff --git a/Makefile.PL b/Makefile.PL
index ed71e84..0ae9903 100644
--- a/Makefile.PL
+++ b/Makefile.PL
@@ -1,6 +1,7 @@
 use inc::Module::Install;
 RTx('RT-Extension-FormTools');
 requires_rt('5.0.0');
+requires('UUID::Tiny');
 
 repository('https://github.com/bestpractical/rt-extension-formtools');
 
diff --git a/html/Admin/FormTools/Modify.html b/html/Admin/FormTools/Modify.html
index 95e40dd..338b55a 100644
--- a/html/Admin/FormTools/Modify.html
+++ b/html/Admin/FormTools/Modify.html
@@ -8,51 +8,362 @@
     <&| /Widgets/TitleBox, title => loc('FormTools Components') &>
       <div class="d-block text-center">
 % foreach my $item ( @html_components ) {
-    <p id="formtools-element-<% $item %>" class="m-1 p-2 border border-primary rounded" draggable="true" ondragstart="dragstart_handler(event);" ondragend="dragend_handler(event);"><% uc($item) %> Element</p>
+        <div id="formtools-element-<% $item %>" class="formtools-element" draggable="true" ondragstart="formTools.dragstart(event);" ondragend="formTools.dragend(event);" data-value="<% JSON({ type => 'raw_html', html => "<$item>test</$item>"}) %>">
+          <p class="formtools-element-placeholder m-1 p-2 border rounded"><% loc('Put it here') %></p>
+          <p class="m-1 p-2 border rounded">
+            <span class="badge badge-primary">raw_html</span>
+            <span class="content"><% loc('[_1] Element', uc($item)) %></span>
+%           if ( $item ne 'hr' ) {
+            <a href="#" class="edit" data-toggle="modal" data-target="#formtools-element-modal">
+              <span class="fas fa-pencil-alt" alt="<% loc('Edit') %>" data-toggle="tooltip" data-placement="top" data-original-title="<% loc('Edit') %>"></span>
+            </a>
+%           }
+            <a href="#" class="remove" onclick="jQuery(this).find('[data-toggle=tooltip]').tooltip('hide'); this.closest('.formtools-element').remove(); return false;">
+              <span class="far fa-times-circle" alt="<% loc('Remove') %>" data-toggle="tooltip" data-placement="top" data-original-title="<% loc('Remove') %>"></span>
+            </a>
+          </p>
+%         if ( $item ne 'hr' ) {
+          <div class="modal fade formtools-element-modal" id="formtools-element-<% $item %>-modal" tabindex="-1" role="dialog">
+            <div class="modal-dialog" role="document">
+              <div class="modal-content">
+                <form class="formtools-element-form">
+                  <div class="modal-header">
+                    <h5 class="modal-title"><% loc('Modify Element') %></h5>
+                    <a href="javascript:void(0)" class="close" data-dismiss="modal" aria-label="Close">
+                      <span aria-hidden="true">×</span>
+                    </a>
+                  </div>
+                  <div class="modal-body">
+                    <textarea name="html" class="form-control"><% "<$item>Text</$item>" %></textarea>
+                  </div>
+                  <div class="modal-footer">
+                    <button type="submit" class="btn btn-primary button form-control"><% loc('Save') %></button>
+                  </div>
+                </form>
+              </div>
+            </div>
+          </div>
+%         }
+        </div>
 % }
-      <hr />
+        <hr />
 % foreach my $item ( @core_components ) {
-    <p id="formtools-element-<% $item %>" class="m-1 p-2 border border-primary rounded" draggable="true" ondragstart="dragstart_handler(event);" ondragend="dragend_handler(event);"><% ucfirst($item) %></p>
+        <div id="formtools-element-<% $item %>" class="formtools-element" draggable="true" ondragstart="formTools.dragstart(event);" ondragend="formTools.dragend(event);" data-value="<% JSON({ type => 'component', comp_name => 'Field', arguments => { name => $item } }) %>">
+          <p class="formtools-element-placeholder m-1 p-2 border rounded"><% loc('Put it here') %></p>
+          <p class="m-1 p-2 border rounded">
+            <span class="badge badge-primary">component</span>
+            <% $item %>
+            <a href="#" class="edit" data-toggle="modal" data-target="#formtools-element-modal">
+              <span class="fas fa-pencil-alt" alt="<% loc('Edit') %>" data-toggle="tooltip" data-placement="top" data-original-title="<% loc('Edit') %>"></span>
+            </a>
+            <a href="#" class="remove" onclick="jQuery(this).find('[data-toggle=tooltip]').tooltip('hide'); this.closest('.formtools-element').remove(); return false;">
+              <span class="far fa-times-circle" alt="<% loc('Remove') %>" data-toggle="tooltip" data-placement="top" data-original-title="<% loc('Remove') %>"></span>
+            </a>
+          </p>
+          <div class="modal fade formtools-element-modal" id="formtools-element-<% $item %>-modal" tabindex="-1" role="dialog">
+            <div class="modal-dialog" role="document">
+              <div class="modal-content">
+                <form class="formtools-element-form">
+                  <div class="modal-header">
+                    <h5 class="modal-title"><% loc('Modify Element') %></h5>
+                    <a href="javascript:void(0)" class="close" data-dismiss="modal" aria-label="Close">
+                      <span aria-hidden="true">×</span>
+                    </a>
+                  </div>
+                  <div class="modal-body">
+                    <div class="form-row">
+                      <div class="col-3 label"><&|/l&>Label</&>:</div>
+                      <div class="col-9 value">
+                        <input name="label" type="text" class="form-control" placeholder="<% $item %>" value="" />
+                      </div>
+                    </div>
+                    <div class="form-row">
+                      <div class="col-3 label"><&|/l&>Default Value</&>:</div>
+                      <div class="col-9 value">
+                        <input name="default" type="text" class="form-control" placeholder="<% $default_values{$item} %>" value="" />
+                      </div>
+                    </div>
+                  </div>
+                  <div class="modal-footer">
+                    <button type="submit" class="btn btn-primary button form-control"><% loc('Save') %></button>
+                  </div>
+                </form>
+              </div>
+            </div>
+          </div>
+        </div>
 % }
-      <hr />
+        <hr />
 % foreach my $item ( @custom_fields ) {
-    <p id="formtools-element-<% $item %>" class="m-1 p-2 border border-primary rounded" draggable="true" ondragstart="dragstart_handler(event);" ondragend="dragend_handler(event);"><% uc($item) %></p>
+        <div id="formtools-element-<% $item %>" class="formtools-element" draggable="true" ondragstart="formTools.dragstart(event);" ondragend="formTools.dragend(event);" data-value="<% JSON({ type => 'component', comp_name => 'Field', arguments => { name => $item } }) %>">
+          <p class="formtools-element-placeholder m-1 p-2 border rounded"><% loc('Put it here') %></p>
+          <p class="m-1 p-2 border rounded">
+            <span class="badge badge-primary">component</span>
+            <% $item %>
+            <a href="#" class="edit" data-toggle="modal" data-target="#formtools-element-modal">
+              <span class="fas fa-pencil-alt" alt="<% loc('Edit') %>" data-toggle="tooltip" data-placement="top" data-original-title="<% loc('Edit') %>"></span>
+            </a>
+            <a href="#" class="remove" onclick="jQuery(this).find('[data-toggle=tooltip]').tooltip('hide'); this.closest('.formtools-element').remove(); return false;">
+              <span class="far fa-times-circle" alt="<% loc('Remove') %>" data-toggle="tooltip" data-placement="top" data-original-title="<% loc('Remove') %>"></span>
+            </a>
+          </p>
+          <div class="modal fade formtools-element-modal" id="formtools-element-<% $item %>-modal" tabindex="-1" role="dialog">
+            <div class="modal-dialog" role="document">
+              <div class="modal-content">
+                <form class="formtools-element-form">
+                  <div class="modal-header">
+                    <h5 class="modal-title"><% loc('Modify Element') %></h5>
+                    <a href="javascript:void(0)" class="close" data-dismiss="modal" aria-label="Close">
+                      <span aria-hidden="true">×</span>
+                    </a>
+                  </div>
+                  <div class="modal-body">
+                    <div class="form-row">
+                      <div class="col-3 label"><&|/l&>Label</&>:</div>
+                      <div class="col-9 value">
+                        <input name="label" type="text" class="form-control" placeholder="<% $item %>" value="" />
+                      </div>
+                    </div>
+                    <div class="form-row">
+                      <div class="col-3 label"><&|/l&>Default Value</&>:</div>
+                      <div class="col-9 value">
+                        <input name="default" type="text" class="form-control" placeholder="<% $default_values{$item} %>" value="" />
+                      </div>
+                    </div>
+                  </div>
+                  <div class="modal-footer">
+                    <button type="submit" class="btn btn-primary button form-control"><% loc('Save') %></button>
+                  </div>
+                </form>
+              </div>
+            </div>
+          </div>
+        </div>
+% }
+
+        <hr />
+% foreach my $item ( sort keys %other_components ) {
+        <div id="formtools-element-<% $item %>" class="formtools-element" draggable="true" ondragstart="formTools.dragstart(event);" ondragend="formTools.dragend(event);" data-value="<% JSON($other_components{$item}) %>">
+          <p class="formtools-element-placeholder m-1 p-2 border rounded"><% loc('Put it here') %></p>
+          <p class="m-1 p-2 border rounded">
+            <span class="badge badge-primary"><% $other_components{$item}{type} %></span>
+            <span class="content"><% $item %></span>
+%         if ( $item eq 'Hidden' ) {
+            <a href="#" class="edit" data-toggle="modal" data-target="#formtools-element-modal">
+              <span class="fas fa-pencil-alt" alt="<% loc('Edit') %>" data-toggle="tooltip" data-placement="top" data-original-title="<% loc('Edit') %>"></span>
+            </a>
+%         }
+            <a href="#" class="remove" onclick="jQuery(this).find('[data-toggle=tooltip]').tooltip('hide'); this.closest('.formtools-element').remove(); return false;">
+              <span class="far fa-times-circle" alt="<% loc('Remove') %>" data-toggle="tooltip" data-placement="top" data-original-title="<% loc('Remove') %>"></span>
+            </a>
+          </p>
+
+%         if ( $item eq 'Hidden' ) {
+          <div class="modal fade formtools-element-modal" id="formtools-element-<% $item %>-modal" tabindex="-1" role="dialog">
+            <div class="modal-dialog" role="document">
+              <div class="modal-content">
+                <form class="formtools-element-form">
+                  <div class="modal-header">
+                    <h5 class="modal-title"><% loc('Modify Element') %></h5>
+                    <a href="javascript:void(0)" class="close" data-dismiss="modal" aria-label="Close">
+                      <span aria-hidden="true">×</span>
+                    </a>
+                  </div>
+                  <div class="modal-body">
+                    <div class="form-row">
+                      <div class="col-3 label"><&|/l&>Name</&>:</div>
+                      <div class="col-9 value">
+                        <input name="name" type="text" class="form-control" value="" />
+                      </div>
+                    </div>
+                    <div class="form-row">
+                      <div class="col-3 label"><&|/l&>Value</&>:</div>
+                      <div class="col-9 value">
+                        <input name="value" type="text" class="form-control" value="" />
+                      </div>
+                    </div>
+                  </div>
+                  <div class="modal-footer">
+                    <button type="submit" class="btn btn-primary button form-control"><% loc('Save') %></button>
+                  </div>
+                </form>
+              </div>
+            </div>
+          </div>
+%         }
+        </div>
 % }
       </div>
     </&>
   </div>
-<div class="formtools-form-pages boxcontainer col-md-9" id="formtools-pages-wrapper">
-<&| /Widgets/TitleBox, title => loc('FormTools Pages') &>
-% my @form_pages = keys %{$form->{'formtools-pages'}};
-  <ul class="nav nav-<% $nav_type %>s" id="formtools-pages">
-% my $current_context = {};
-% foreach my $page_name (@form_pages) {
-%     my $tab_id = CSSClass( $page_name );
-%     $current_context->{tab} = $tab_id;
-% #     my( $active, $aria_selected) = $tab_id eq $active_context->{tab} ? ('active', 'true') : ('', 'false');
-%     my( $active, $aria_selected) = ('active', 'true');
-%     my $nav_id = join '-', 'nav', $current_context->{tab};
-%     my $content_id = join '-', 'content', $current_context->{tab};
-    <li class="nav-item">
-      <a class="nav-link <% $active %>" id="<% $nav_id %>" data-toggle="<% $nav_type %>" href="#<% $content_id %>" role="<% $nav_type %>" aria-controls="<% $content_id %>" aria-selected="<% $aria_selected %>"><% $page_name %></a>
-    </li>
+  <div class="formtools-form-pages boxcontainer col-md-9" id="formtools-pages-wrapper">
+  <&| /Widgets/TitleBox, title => loc('FormTools Pages') &>
+
+    <ul class="nav nav-<% $nav_type %>s" id="formtools-pages">
+%   my $current_context = {};
+%   foreach my $page_name (@form_pages) {
+%     my( $active, $aria_selected) = $form_page_id{$page_name} eq $active_context->{tab} ? ('active', 'true') : ('', 'false');
+%     my $nav_id = join '-', 'formtools', 'nav', $form_page_id{$page_name};
+%     my $content_id = join '-', 'formtools', 'content', $form_page_id{$page_name};
+      <li class="nav-item">
+        <a class="nav-link <% $active %>" id="formtools-tab-<% $nav_id %>" data-toggle="<% $nav_type %>" href="#<% $content_id %>" role="<% $nav_type %>" aria-controls="formtools-<% $content_id %>" aria-selected="<% $aria_selected %>"><% $form->{'formtools-pages'}{$page_name}{name} %></a>
+      </li>
 % }
-  </ul>
-  <p>Drag components from the left toolbar and drop them here</p>
-    <div id="formtools-content-form-1" class="formtools-content w-100 border border-primary rounded" ondrop="drop_handler(event);" ondragover="dragover_handler(event);" ondragend="dragend_handler(event);">
+      <li class="nav-item">
+        <a class="nav-link" id="formtools-tab-add" href="?id=<% $id %>;AddPage=1">
+          <span class="fas fa-plus" alt="<% loc('Add Page') %>" data-toggle="tooltip" data-original-title="<% loc('Add Page') %>"></span>
+        </a>
+      </li>
+    </ul>
+    <div class="tab-content">
+%   foreach my $page_name (@form_pages) {
+      <div id="formtools-content-<% $form_page_id{$page_name} %>" class="tab-pane <% $form_page_id{$page_name} eq $active_context->{tab} ? 'show active' : 'fade' %>" role="tabpanel">
+        <&| /Widgets/TitleBox, title => loc('Basic Info'), titleright_raw => qq{} &>
+        <form>
+          <div class="form-row">
+            <div class="col-3 label"><&|/l&>Name</&>:</div>
+            <div class="col-9 value">
+              <input name="name" class="form-control" value="<% $form->{'formtools-pages'}{$page_name}{name} %>" />
+            </div>
+          </div>
+          <div class="form-row">
+            <div class="col-3 label"><&|/l&>Sort order</&>:</div>
+            <div class="col-9 value">
+              <input name="sort_order" class="form-control" value="<% $form->{'formtools-pages'}{$page_name}{sort_order} %>" />
+            </div>
+          </div>
+          <div class="form-row">
+            <div class="col-3 label"></div>
+            <div class="col-9 value">
+              <div class="custom-control custom-checkbox">
+                <input class="custom-control-input" id="<% $form_page_id{$page_name} %>-validation" type="checkbox" name="validation" value="1" <% $form->{'formtools-pages'}{$page_name}{validation} ? 'checked="checked"' : '' |n%> />
+                <label class="custom-control-label" for="<% $form_page_id{$page_name} %>-validation">
+                  <&|/l&>Enable validation</&>
+                </label>
+              </div>
+            </div>
+          </div>
+%         # Do not delete the last one
+%         if ( $form->{'formtools-pages'}{$page_name}{next} ) {
+          <div class="form-row">
+            <div class="col-3 label"></div>
+            <div class="col-9 value">
+              <a class="formtools-delete-page btn btn-primary button" data-page="<% $page_name %>" href="#"><% loc('Delete Page') %></a>
+            </div>
+          </div>
+%         }
+        </form>
+        </&>
+
+        <&| /Widgets/TitleBox, title => loc('Content') &>
+          <div class="formtools-content w-100 border rounded" data-page="<% $page_name %>" data-page-id="<% $form_page_id{$page_name} %>" ondrop="formTools.drop(event);" ondragover="formTools.dragover(event);" ondragend="formTools.dragend(event);">
+%         my $i = 0;
+%         for my $item ( grep { $_->{type} ne 'hidden' || $_->{'input-name'} ne 'create_ticket' } @{$form->{'formtools-pages'}{$page_name}{content} || []} ) {
+            <div id="formtools-element-<% $form_page_id{$page_name} %>-<% $i %>" class="formtools-element" draggable="true" ondragstart="formTools.dragstart(event);" ondragend="formTools.dragend(event);" ondragenter="formTools.dragenter(event)" ondragleave="formTools.dragleave(event)" data-value="<% JSON($item) %>">
+              <p class="formtools-element-placeholder m-1 p-2 border rounded"><% loc('Put it here') %></p>
+              <p class="m-1 p-2 border rounded">
+                <span class="badge badge-primary"><% $item->{type} %></span>
+                <span class="content">
+%               if ( $item->{type} eq 'raw_html' ) {
+                  <% length $item->{html} > 40 ? substr($item->{html}, 0, 40) . '...' : $item->{html} %>
+%               } elsif ( $item->{type} eq 'hidden' ) {
+                  <% $item->{'input-name'} %>: <% $item->{'input-value'} %>
+%               } else {
+                  <% $item->{arguments}{name} || $item->{comp_name} %>
+%               }
+                </span>
+%             if ( $item->{type} eq 'raw_html' || ( $item->{type} eq 'component' && $item->{comp_name} eq 'Field' ) || $item->{type} eq 'hidden' ) {
+                <a href="#" class="edit" data-toggle="modal" data-target="#formtools-element-<% $form_page_id{$page_name} %>-<% $i %>-modal">
+                  <span class="fas fa-pencil-alt" alt="<% loc('Edit') %>" data-toggle="tooltip" data-placement="top" data-original-title="<% loc('Edit') %>"></span>
+                </a>
+%             }
+                <a href="#" class="remove" onclick="jQuery(this).find('[data-toggle=tooltip]').tooltip('hide'); this.closest('.formtools-element').remove(); return false;">
+                  <span class="far fa-times-circle" alt="<% loc('Remove') %>" data-toggle="tooltip" data-placement="top" data-original-title="<% loc('Remove') %>"></span>
+                </a>
+              </p>
+              <div class="modal fade formtools-element-modal" id="formtools-element-<% $form_page_id{$page_name} %>-<% $i %>-modal" tabindex="-1" role="dialog">
+                <div class="modal-dialog" role="document">
+                  <div class="modal-content">
+                    <form class="formtools-element-form">
+                      <div class="modal-header">
+                        <h5 class="modal-title"><% loc('Modify Element') %></h5>
+                        <a href="javascript:void(0)" class="close" data-dismiss="modal" aria-label="Close">
+                          <span aria-hidden="true">×</span>
+                        </a>
+                      </div>
+                      <div class="modal-body">
+%                     if ( $item->{type} eq 'raw_html' ) {
+                        <textarea name="html" class="form-control"><% $item->{html} %></textarea>
+%                     } elsif ( $item->{type} eq 'component' && $item->{comp_name} eq 'Field' ) {
+                        <div class="form-row">
+                          <div class="col-3 label"><&|/l&>Label</&>:</div>
+                          <div class="col-9 value">
+                            <input name="label" type="text" class="form-control" placeholder="<% $item->{arguments}{name} %>" value="<% $item->{arguments}{label} // ''  %>" />
+                          </div>
+                        </div>
+                        <div class="form-row">
+                          <div class="col-3 label"><&|/l&>Default Value</&>:</div>
+                          <div class="col-9 value">
+                            <input name="default" type="text" class="form-control" placeholder="<% $default_values{$item->{arguments}{name}} %>" value="<% $item->{arguments}{default} // '' %>" />
+                          </div>
+                        </div>
+%                     } elsif ( $item->{type} eq 'hidden' ) {
+                        <div class="form-row">
+                          <div class="col-3 label"><&|/l&>Name</&>:</div>
+                          <div class="col-9 value">
+                            <input name="name" type="text" class="form-control" value="<% $item->{'input-name'} // ''  %>" />
+                          </div>
+                        </div>
+                        <div class="form-row">
+                          <div class="col-3 label"><&|/l&>Value</&>:</div>
+                          <div class="col-9 value">
+                            <input name="value" type="text" class="form-control" value="<% $item->{'input-value'} // '' %>" />
+                          </div>
+                        </div>
+%                     }
+                      </div>
+                      <div class="modal-footer">
+
+                        <button type="submit" class="btn btn-primary button form-control"><% loc('Save') %></button>
+                      </div>
+                    </form>
+                  </div>
+                </div>
+              </div>
+            </div>
+%           $i++;
+%         }
+            <p class="formtools-element-placeholder m-1 p-2 border rounded"><% loc('Put it here') %></p>
+          </div>
+
+
+        </&>
+      </div>
+%   }
     </div>
-</&>
-</div><!-- row -->
+
+    <form method="POST" action="Modify.html" id="formtools-form-modify">
+      <input type="hidden" name="id" value="<% $id %>">
+      <input type="hidden" name="ActiveTab" value="">
+      <input type="hidden" name="Content">
+      <div class="row">
+        <div class="col-12" style="padding-right: 25px">
+          <& /Elements/Submit, Name => 'Update', Label => loc('Save Changes') &>
+        </div>
+      </div>
+    </form>
+  </&>
+  </div><!-- row -->
 </div><!-- formtools-form-pages -->
 </div><!-- formtools-edit -->
 
-<p><&|/l&>Loaded form <% $form_attribute->Description %></&></p>
-
-<pre>
-% use Data::Printer;
-% $m->out(np($form));
-</pre>
-
+<script type="text/javascript">
+jQuery(function() {
+    jQuery('#formtools-form-modify').on('submit', formTools.submit);
+    jQuery('.formtools-element-form').on('submit', formTools.elementSubmit);
+    jQuery('.formtools-delete-page').on('click', formTools.deletePage);
+});
+</script>
 <%INIT>
 
 Abort("No form id found") unless $id;
@@ -66,16 +377,146 @@ unless ( $ok ) {
 
 my $form = $form_attribute->Content;
 
-my ($title, @results);
-$title = loc("Modify form [_1]", $form_attribute->Description);
+use UUID::Tiny 'create_uuid_as_string';
+
+my @results;
+
+if ( $AddPage ) {
+    Abort( loc('Permission Denied') )
+        unless $session{'CurrentUser'}->HasRight( Object => $RT::System, Right => 'SuperUser' );
+
+    my @orders = map { $form->{'formtools-pages'}{$_}{sort_order} } keys %{$form->{'formtools-pages'}};
+
+    my $new_page = create_uuid_as_string();
+    $form->{'formtools-pages'}{$new_page} = {
+        name       => 'Page New',
+        sort_order => ( $orders[-2] || 0 ) + 1,
+    };
+    my ( $ret, $msg ) = $form_attribute->SetContent($form);
+    if ($ret) {
+        push @results, loc('Updated content');
+    }
+    else {
+        push @results, loc( "Couldn't update content: [_1]", $msg );
+    }
+
+    MaybeRedirectForResults(
+        Actions   => \@results,
+        Path      => '/Admin/FormTools/Modify.html',
+        Arguments => { id => $id, ActiveTab => $new_page },
+    );
+}
+elsif ( $Update ) {
+    Abort( loc('Permission Denied') )
+        unless $session{'CurrentUser'}->HasRight( Object => $RT::System, Right => 'SuperUser' );
+
+    my $new_content = eval { JSON::from_json( $ARGS{Content} ) };
+    if ( $@ ) {
+        push @results, loc( "Couldn't decode JSON" );
+    }
+    else {
+        require List::Util;
+        my %order_name = map { $new_content->{$_}{sort_order} => $_ } keys %$new_content;
+        my @orders = sort { $a <=> $b } keys %order_name;
+
+        my ( $first_page, $submit_page );
+        for my $page ( sort { $new_content->{$a}{sort_order} <=> $new_content->{$b}{sort_order} } keys %$new_content ) {
+            if ( $new_content->{$page}{sort_order} == $orders[0] ) {
+                $form->{'formtools-start-page'} = $page;
+            }
+
+            if ( $orders[-2] && $new_content->{$page}{sort_order} == $orders[-2] ) {
+                push @{ $new_content->{$page}{content} },
+                    { type => 'hidden', 'input-name' => 'create_ticket', 'input-value' => 'create_ticket' };
+            }
+
+            if ( my $next_order = List::Util::first { $_ > $new_content->{$page}{sort_order} } @orders ) {
+                $new_content->{$page}{next} = $order_name{$next_order};
+            }
+            else {
+                $new_content->{$page}{next} = '';
+            }
+        }
+
+        $form->{'formtools-pages'} = $new_content;
+
+        if ( $form_attribute->_SerializeContent($form) ne $form_attribute->_Value('Content') ) {
+            my ( $ret, $msg ) = $form_attribute->SetContent($form);
+            if ($ret) {
+                push @results, loc('Updated content');
+            }
+            else {
+                push @results, loc( "Couldn't update content: [_1]", $msg );
+            }
+        }
+    }
+
+    MaybeRedirectForResults(
+        Actions   => \@results,
+        Path      => '/Admin/FormTools/Modify.html',
+        Arguments => { id => $id, ActiveTab => $ActiveTab },
+    );
+}
+
+my $title = loc("Modify form [_1]", $form_attribute->Description);
 
 my $nav_type = 'pill'; # 'tab' or 'pill'
 
-my @html_components = qw( h1 h2 h3 hr );
-my @core_components = qw( requestors owner subject content );
-my @custom_fields = qw( cf1 cf2 cf3 );
+my @html_components = qw( h1 h2 h3 h4 h5 h6 hr p );
+my @core_components = qw( Requestors Owner Subject Content );
+
+my $queue = RT::Queue->new($session{'CurrentUser'});
+$queue->Load($form->{queue});
+my $cfs = $queue->TicketCustomFields;
+my @custom_fields;
+my %default_values;
+
+while ( my $cf = $cfs->Next ) {
+    push @custom_fields, $cf->Name;
+    if ( $cf->SupportDefaultValues ) {
+        if ( defined( my $default_values = $cf->DefaultValues(Object => $queue) ) ) {
+            $default_values{$cf->Name} = ref $default_values eq 'ARRAY' ? join(', ', @$default_values) : $default_values;
+        }
+    }
+}
+
+my %other_components = (
+    ShowChoices => { type => 'component', comp_name => 'ShowChoices' },
+    Hidden      => { type => 'hidden' },
+);
+
+
+
+$form->{'formtools-pages'} ||= {
+    create_uuid_as_string => { sort_order => 1, name => 'Page 1' },
+    create_uuid_as_string => {
+        sort_order => 999,
+        name       => 'Result',
+        content    => [
+            {
+                type => 'raw_html',
+                html => '<h2>Request Submitted</h2>',
+            },
+            {
+                type => 'raw_html',
+                html => '<p>Your request has been submitted.</p>',
+            },
+        ],
+    },
+};
+
+
+my @form_pages
+    = sort { ( $form->{'formtools-pages'}{$a}{sort_order} || 0 ) <=> ( $form->{'formtools-pages'}{$b}{sort_order} || 0 ) }
+    keys %{ $form->{'formtools-pages'} };
+
 
+my %form_page_id = map { $_ => CSSClass($_) } @form_pages;
+my $active_context = { tab => $ActiveTab || $form_page_id{$form_pages[0]} };
 </%INIT>
 <%ARGS>
 $id => undef
+$ActiveTab => ''
+$Update => undef
+$AddPage => undef
 </%ARGS>
diff --git a/static/css/rt-extension-formtools.css b/static/css/rt-extension-formtools.css
index ad60315..f9288f3 100644
--- a/static/css/rt-extension-formtools.css
+++ b/static/css/rt-extension-formtools.css
@@ -2,3 +2,44 @@
 div .formtools-content {
     min-height: 400px;
 }
+
+.formtools-form-pages .badge {
+    background-color: #4868b3;
+}
+
+.formtools-form-pages .remove {
+    position: absolute;
+    right: 2.5em;
+    color: #5C6273;
+}
+
+.formtools-component-menu .remove {
+    display: none;
+}
+
+.formtools-form-pages .edit {
+    position: absolute;
+    right: 4em;
+    color: #5C6273;
+}
+
+.formtools-component-menu .edit {
+    display: none;
+}
+
+.formtools-component-menu .badge {
+    display: none;
+}
+
+.formtools-element-placeholder {
+    border: 2px dotted #ffc107 !important;
+    display: none;
+}
+
+.formtools-element-placeholder.active {
+    display: block;
+}
+
+.formtools-element.current p {
+    opacity: 0.7;
+}
\ No newline at end of file
diff --git a/static/js/rt-extension-formtools.js b/static/js/rt-extension-formtools.js
index 31c5d6e..905714b 100644
--- a/static/js/rt-extension-formtools.js
+++ b/static/js/rt-extension-formtools.js
@@ -1,69 +1,147 @@
-function dragstart_handler(ev) {
-  console.log("dragStart");
-  var dti = ev.dataTransfer.items;
-  if (dti === undefined || dti == null) {
-    console.log("Browser does not support DataTransferItem interface");
-    return;
-  }
-
-  // Add the id of the drag source element to the drag data payload so
-  // it is available when the drop event is fired
-  dti.add(ev.target.id, "text/plain");
-  // Tell the browser both copy and move are possible
-  ev.effectAllowed = "copy";
-}
-
-function dragover_handler(ev) {
-    console.log("dragOver");
-    var dti = ev.dataTransfer.items;
-    if (dti === undefined || dti == null) {
-        console.log("Browser does not support DataTransferItem interface");
-        return;
-    }
-    // Change the target element's border to signify a drag over event
-    // has occurred
-    ev.currentTarget.style.background = "lightgray";
-    ev.preventDefault();
-}
-
-function drop_handler(ev) {
-    console.log("Drop");
-    ev.preventDefault();
-    var dti = ev.dataTransfer.items;
-    if (dti === undefined || dti == null) {
-        console.log("Browser does not support DataTransferItem interface");
-        return;
-    }
-    // Get the id of the drag source element (that was added to the drag data
-    // payload by the dragstart event handler). Even though only one drag item
-    // was explicitly added, the browser may include other items so need to search
-    // for the plain/text item.
-    for (var i=0; i < dti.length; i++) {
-        console.log("Drop: item[" + i + "].kind = " + dti[i].kind + " ; item[" + i + "].type = " + dti[i].type);
-        if ((dti[i].kind == 'string') && (dti[i].type.match('^text/plain'))) {
-          // This item is the target node
-          dti[i].getAsString(function (id){
-              // Copy the element
-              var nodeCopy = document.getElementById(id).cloneNode(true);
-              console.log("Copying " + nodeCopy);
-              nodeCopy.id = "newId";
-              ev.target.appendChild(nodeCopy);
-          });
+formTools = {
+    dragstart: function (ev) {
+        ev.dataTransfer.setData("text/plain", ev.target.id);
+        // Tell the browser both copy and move are possible
+        ev.effectAllowed = "copy";
+
+        jQuery(ev.target).addClass('current');
+        jQuery(ev.target).find('.formtools-element-placeholder').addClass('hidden');
+        jQuery(ev.target).next().find('.formtools-element-placeholder').addClass('hidden');
+    },
+
+    dragenter: function (ev) {
+        ev.preventDefault();
+        jQuery(ev.target).closest('.formtools-content').find('.formtools-element-placeholder').removeClass('active');
+        jQuery(ev.target).closest('.formtools-element').children('.formtools-element-placeholder').addClass('active');
+    },
+
+    dragleave: function (ev) {
+        ev.preventDefault();
+    },
+
+    dragover: function (ev) {
+        ev.preventDefault();
+        if ( ev.target.classList.contains('formtools-content') ) {
+            const last_element = jQuery(ev.target).find('.formtools-element').get(-1);
+            if ( last_element ) {
+                const last_position = last_element.getBoundingClientRect();
+                if ( ev.y > last_position.y + last_position.height ) {
+                    jQuery(ev.target).find('.formtools-element .formtools-element-placeholder').removeClass('active');
+                    jQuery(ev.target).children('.formtools-element-placeholder').addClass('active');
+                }
+            }
         }
-    }
+    },
 
-    // Clear background
-    ev.currentTarget.style.background = "none";
-}
+    drop: function (ev) {
+        ev.preventDefault();
 
-function dragend_handler(ev) {
-    console.log("dragEnd");
-    var dti = ev.dataTransfer.items;
-    if (dti === undefined || dti == null) {
-        console.log("Browser does not support DataTransferItem interface");
-        return;
-    }
+        const source = document.getElementById(ev.dataTransfer.getData("text"));
+
+        const sibling = ev.target.closest('.formtools-element');
+        const area = ev.target.closest('.formtools-content');
+        if ( source.closest('.formtools-content') ) {
+            if ( sibling ) {
+                area.insertBefore(source, sibling);
+            }
+            else {
+                area.insertBefore(source, area.children[area.children.length-1]);
+            }
+        }
+        else {
+            const source_copy = source.cloneNode(true);
+
+            const old_id = source_copy.id;
+            source_copy.id = 'formtools-element-' + area.dataset.pageId + '-' + Date.now();
+            jQuery(source_copy).find('#' + old_id + '-modal').attr('id', source_copy.id + '-modal' );
+            jQuery(source_copy).find('a.edit').attr('data-target', '#' + source_copy.id + '-modal' );
+            jQuery(source_copy).find('form.formtools-element-form').on('submit', formTools.elementSubmit);
+            jQuery(source_copy).find('.formtools-element-modal').modal('show');
+            jQuery(source_copy).attr('ondragenter', 'formTools.dragenter(event);');
+            if ( sibling ) {
+                area.insertBefore(source_copy, sibling);
+            }
+            else {
+                area.insertBefore(source_copy, area.children[area.children.length-1]);
+            }
+        }
+    },
+
+    dragend: function (ev) {
+        jQuery('.formtools-content:visible').find('.formtools-element-placeholder').removeClass('active hidden');
+        jQuery('.formtools-content:visible').find('.formtools-element').removeClass('current');
+        jQuery('.formtools-component-menu').find('.formtools-element').removeClass('current');
+    },
 
-    // Remove all of the items from the list.
-    dti.clear();
-}
+    elementSubmit: function(e) {
+        e.preventDefault();
+        const form = jQuery(this);
+        const element = form.closest('.formtools-element');
+        const value = element.data('value');
+
+        if ( value.type === 'raw_html' ) {
+            value.html = form.find(':input[name=html]').val();
+            element.find('span.content').text(value.html.length > 40 ? value.html.substr(0, 40) + '...' : value.html);
+        }
+        else if ( value.type === 'component' && value.comp_name === 'Field' ) {
+            const label = form.find(':input[name=label]').val();
+            if ( label.length ) {
+                value.arguments.label = label;
+            }
+            else {
+                delete value.arguments.label;
+            }
+
+            const default_value = form.find(':input[name=default]').val();
+
+            if ( default_value.length ) {
+                value.arguments.default = default_value;
+            }
+            else {
+                delete value.arguments.default;
+            }
+        }
+        else if ( value.type === 'hidden' ) {
+            value['input-name'] = form.find(':input[name=name]').val();
+            value['input-value'] = form.find(':input[name=value]').val();
+            element.find('span.content').text(value['input-name'] + ': ' + value['input-value']);
+        }
+        element.data('value', value);
+        form.closest('.formtools-element-modal').modal('hide');
+    },
+
+    submit: function(e) {
+        const form = jQuery(this);
+        const content = {};
+        jQuery('div.formtools-content').each(function() {
+            let page = jQuery(this).data('page');
+            content[page] ||= {};
+
+            for ( let attr of ['name', 'sort_order', 'validation'] ) {
+                if ( attr === 'validation' ) {
+                    content[page][attr] = jQuery(this).closest('div.tab-pane').find(':input[name="' + attr + '"]').is(':checked') ? 1 : 0;
+                }
+                else {
+                    content[page][attr] = jQuery(this).closest('div.tab-pane').find(':input[name="' + attr + '"]').val();
+                }
+            }
+
+            content[page]['content'] ||= [];
+            jQuery(this).children('.formtools-element').each(function() {
+                content[page]['content'].push(jQuery(this).data('value'));
+            });
+        });
+        form.find('input[name=ActiveTab]').val(jQuery('.formtools-content:visible').data('page-id'));
+        form.find('input[name=Content]').val(JSON.stringify(content));
+    },
+
+    deletePage: function() {
+        const tab = jQuery(this).closest('.tab-pane');
+
+        tab.fadeOut(function() {
+            jQuery('#formtools-pages').find('a.nav-link[href="#' + tab.attr('id') + '"]').closest('li').remove();
+            jQuery('#formtools-pages').find('li:first a.nav-link').tab('show');
+        }).remove();
+        return false;
+    }
+};

commit 7bdb20a3ab4b724e7027f0dc4ebedb60b5ff0e78
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Tue Sep 26 17:08:40 2023 -0400

    Build the correct path to render forms in SelfService

diff --git a/html/Forms/dhandler b/html/Forms/dhandler
index 790aece..c49a320 100644
--- a/html/Forms/dhandler
+++ b/html/Forms/dhandler
@@ -1,6 +1,6 @@
-<&|/FormTools/Form, next => '/Forms/' . $form_name . '/' . $form_config->{'formtools-pages'}{$page}{'next'},
+<&|/FormTools/Form, next => $base_path . $form_name . '/' . $form_config->{'formtools-pages'}{$page}{'next'},
     validation => $form_config->{'formtools-pages'}{$page}{'validation'},
-    next_for_validation => '/Forms/' . $form_name . '/' . $page,
+    next_for_validation => $base_path . $form_name . '/' . $page,
     results_ref => \@results,
 &>
 
@@ -83,6 +83,9 @@ unless ( $ok ) {
 $m->notes( queue => $queue_obj );
 $m->notes( page_title => $form_config->{'formtools-pages'}{$page}{'name'} );
 
+my $base_path = '/Forms/';
+$base_path = '/SelfService' . $base_path if $SelfService;
+
 # Try to create a ticket if we're on the last page and
 # "create_ticket" is submitted as an arg from the second-to-last
 # page.
@@ -102,4 +105,5 @@ if ( $create_ticket ) {
 <%args>
 $_form_tools_next => undef
 $create_ticket => undef
+$SelfService => 0
 </%args>
diff --git a/html/SelfService/Forms/dhandler b/html/SelfService/Forms/dhandler
index 62e18bf..a2aad9f 100644
--- a/html/SelfService/Forms/dhandler
+++ b/html/SelfService/Forms/dhandler
@@ -1,2 +1,2 @@
 % # Shim for SelfService to run the Forms page
-% $m->comp('/Forms/dhandler', %ARGS);
+% $m->comp('/Forms/dhandler', SelfService => 1, %ARGS);

commit e82b385f37d5b5885fe3dbb347d843bdc235797a
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Tue Sep 26 17:05:47 2023 -0400

    Don't die if a custom field can't be loaded
    
    The most common case is the end user doesn't have
    rights to see the CF, and this shouldn't cause a
    server error for the entire form. Leave an error message
    in the logs for the admin and return.

diff --git a/html/FormTools/Field b/html/FormTools/Field
index ca20a17..5ffd333 100644
--- a/html/FormTools/Field
+++ b/html/FormTools/Field
@@ -53,7 +53,8 @@ if ( RT::Extension::FormTools::is_core_field($name) ) {
     unless ( $cf->id ) {
         my $msg = "Could not find a custom field called $name";
         $msg .= " for the queue ".$queue->Name if (defined $queue);
-        die $msg;
+        RT->Logger->error('Could not load custom field:' . $msg);
+        return;
     }
 
     $m->notes( cfs_on_page => [ @{$m->notes('cfs_on_page')||[]}, $cf->id ] );
@@ -250,7 +251,8 @@ $default = '' unless defined $default;
 % }
 
 % } else {
-% die "'$render_as' isn't a valid rendering option for field '$name'";
+% RT->Logger->error("'$render_as' isn't a valid rendering option for field '$name'");
+% return;
 %}
 
 % }

commit 143c3aaf1c889c6fb7c13f8e6f69afe962879004
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Mon Sep 25 17:01:38 2023 -0400

    Add clickable default graphics for forms

diff --git a/html/Elements/Forms/ListForms b/html/Elements/Forms/ListForms
index 63dc6e2..73185f1 100644
--- a/html/Elements/Forms/ListForms
+++ b/html/Elements/Forms/ListForms
@@ -1,15 +1,21 @@
 <&|/Widgets/TitleBox, title => '', class => 'fullwidth' &>
+<div class="container">
+% my $item_number = 1;
 % while ( my $form_attribute = $forms->Next ) {
 %    my $form = $form_attribute->Content;
 <div class="row border border-primary">
-  <div class="col-4 text-center">
-    <p><% $form_attribute->Description %></p>
+  <div class="col-4">
+    <a href="<% RT->Config->Get('WebPath') %>/SelfService/Forms/<% $form_attribute->Description %>">
+      <% $m->scomp('/Elements/Forms/ShowFormGraphic', FormName => $form_attribute->Description, ItemNumber => $item_number) |n %>
+    </a>
   </div>
-  <div class="col-8 text-left">
-    <% $form->{'form-description'} %>
+  <div class="col">
+    <% $form->{'form-description'} |n %>
   </div>
 </div>
+% $item_number++;
 % }
+</div>
 </&>
 <%init>
 my $forms = RT::Attributes->new( RT->SystemUser );
diff --git a/html/Elements/Forms/ShowFormGraphic b/html/Elements/Forms/ShowFormGraphic
new file mode 100644
index 0000000..37679ea
--- /dev/null
+++ b/html/Elements/Forms/ShowFormGraphic
@@ -0,0 +1,21 @@
+<div class="formtools-form-graphic w-50 mt-2 mb-2">
+  <div class="rounded pt-5 pb-5" style="background-color: <%$color%>;">
+    <p class="text-center" style="font-color: white; font-weight: bold; font-size: 2rem;">
+      <% $FormName %>
+    </p>
+  </div>
+</div>
+<%init>
+
+my $color_list = [
+    "#1f77b4", "#ff7f0e", "#2ca02c", "#d62728",
+    "#9467bd", "#8c564b", "#e377c2", "#7f7f7f"
+];
+
+my $color = $color_list->[ $ItemNumber % 8 ];
+
+</%init>
+<%args>
+$FormName => "Form"
+$ItemNumber => 1
+</%args>
diff --git a/html/SelfService/Forms/dhandler b/html/SelfService/Forms/dhandler
new file mode 100644
index 0000000..62e18bf
--- /dev/null
+++ b/html/SelfService/Forms/dhandler
@@ -0,0 +1,2 @@
+% # Shim for SelfService to run the Forms page
+% $m->comp('/Forms/dhandler', %ARGS);

commit c8a2ec6d9c733d1270db113ea053d796d65d81fa
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Fri Sep 22 18:07:51 2023 -0400

    Set Describe form action to avoid duplicating submitted args like id
    
    Duplicated ids can cause page menu to behave incorrectly at least.

diff --git a/html/Admin/FormTools/Describe.html b/html/Admin/FormTools/Describe.html
index bfd2077..a29ae35 100644
--- a/html/Admin/FormTools/Describe.html
+++ b/html/Admin/FormTools/Describe.html
@@ -4,7 +4,7 @@
 
 <&| /Widgets/TitleBox, title => '' &>
   <p>Forms are shown to users on a page in the RT Self Service interface. Below you can manage how the form details will appear to end users.</p>
-  <form name="EditFormDescription" action="" method="post" enctype="multipart/form-data">
+  <form name="EditFormDescription" action="<%RT->Config->Get('WebPath')%>/Admin/FormTools/Describe.html" method="post" enctype="multipart/form-data">
   <input type="hidden" class="hidden" name="id" value="<% $id %>" />
   <&| /Widgets/TitleBox, title => loc('Icon') &>
     <p>Form to upload an icon</p>

commit 1de8428b9598a7cc49b0d2d8b01ccabc8a6e6aa7
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Fri Sep 22 18:05:00 2023 -0400

    Only super users can update forms

diff --git a/html/Admin/FormTools/Describe.html b/html/Admin/FormTools/Describe.html
index b2c1384..bfd2077 100644
--- a/html/Admin/FormTools/Describe.html
+++ b/html/Admin/FormTools/Describe.html
@@ -46,6 +46,8 @@ my ($title, @results);
 $title = loc("Description for form [_1]", $form_attribute->Description);
 
 if ( $ARGS{'SubmitDescription'} ) {
+    Abort( loc('Permission Denied') )
+        unless $session{'CurrentUser'}->HasRight( Object => $RT::System, Right => 'SuperUser' );
 
     if ( $form->{'form-description'} ne $ARGS{'FormDescription'} ) {
         $form->{'form-description'} = $ARGS{'FormDescription'};

commit 85900317dca80df473fbafa8b76418eef036e1fb
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Fri Sep 22 17:56:25 2023 -0400

    Tweak FormDescription form processing mainly to show end user messages
    
    It also keeps user input(so user can re-submit if needed) in case the
    change fails somehow.

diff --git a/html/Admin/FormTools/Describe.html b/html/Admin/FormTools/Describe.html
index 96aeb4a..b2c1384 100644
--- a/html/Admin/FormTools/Describe.html
+++ b/html/Admin/FormTools/Describe.html
@@ -15,7 +15,7 @@
       <div class="form-row">
         <div class="col-12">
           <input type="hidden" class="hidden" name="FormDescriptionType" value="text/html" />
-          <textarea autocomplete="off" class="form-control messagebox richtext" cols="80" rows="15" name="FormDescription"><%$form->{'form-description'}%></textarea>
+          <textarea autocomplete="off" class="form-control messagebox richtext" cols="80" rows="15" name="FormDescription"><% $ARGS{'FormDescription'} || $form->{'form-description'} %></textarea>
         </div>
       </div>
   </&>
@@ -45,16 +45,23 @@ my $form = $form_attribute->Content;
 my ($title, @results);
 $title = loc("Description for form [_1]", $form_attribute->Description);
 
-if ( $ARGS{'SubmitDescription'} && $ARGS{'SubmitDescription'} eq 'Save' ) {
-    $form->{'form-description'} = $ARGS{'FormDescription'};
+if ( $ARGS{'SubmitDescription'} ) {
 
-
-    $form_attribute->SetContent($form);
+    if ( $form->{'form-description'} ne $ARGS{'FormDescription'} ) {
+        $form->{'form-description'} = $ARGS{'FormDescription'};
+        my ( $ret, $msg ) = $form_attribute->SetContent($form);
+        if ($ret) {
+            push @results, loc('Updated description');
+        }
+        else {
+            push @results, loc( "Could not update description: [_1]", $msg );
+        }
+    }
 }
 
 MaybeRedirectForResults(
     Actions   => \@results,
-    Arguments => { id => $id },
+    Arguments => { id => $id, FormDescription => $ARGS{'FormDescription'} },
 );
 
 </%init>

commit b1e90fe5c3ad0b633fd0580779758ddc67832111
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Fri Sep 22 17:54:53 2023 -0400

    Add FormDescriptionType input so editor knows the input is html

diff --git a/html/Admin/FormTools/Describe.html b/html/Admin/FormTools/Describe.html
index 4267f0c..96aeb4a 100644
--- a/html/Admin/FormTools/Describe.html
+++ b/html/Admin/FormTools/Describe.html
@@ -14,6 +14,7 @@
     <p>An HTML edit box for instructions</p>
       <div class="form-row">
         <div class="col-12">
+          <input type="hidden" class="hidden" name="FormDescriptionType" value="text/html" />
           <textarea autocomplete="off" class="form-control messagebox richtext" cols="80" rows="15" name="FormDescription"><%$form->{'form-description'}%></textarea>
         </div>
       </div>

commit 8f29becf851b0008f2f5c098468f461b24dd8389
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Fri Sep 22 17:29:14 2023 -0400

    Support to edit form name/content via web UI

diff --git a/html/Admin/FormTools/Advanced.html b/html/Admin/FormTools/Advanced.html
new file mode 100644
index 0000000..739c7a2
--- /dev/null
+++ b/html/Admin/FormTools/Advanced.html
@@ -0,0 +1,101 @@
+<& /Admin/Elements/Header, Title => $title &>
+<& /Elements/Tabs &>
+<& /Elements/ListActions, actions => \@results &>
+
+<form action="<%RT->Config->Get('WebPath')%>/Admin/FormTools/Advanced.html" name="ModifyFormToolsAdvanced" method="post" enctype="multipart/form-data" class="mx-auto max-width-md">
+  <input type="hidden" class="hidden" name="id" value="<% $id %>" />
+  <div class="form-row">
+    <div class="col-3 label">
+      <&|/l&>Name</&>:
+    </div>
+    <div class="col-9 value">
+      <input name="Description" class="form-control" value="<% $ARGS{Description} // $form_attribute->Description %>"/>
+    </div>
+  </div>
+  <div class="form-row">
+    <div class="col-3 label">
+      <&|/l&>Content</&>:
+    </div>
+    <div class="col-9 value">
+      <textarea class="form-control" rows="30" name="Content"><% $ARGS{Content} || $encoded_content %></textarea>
+    </div>
+  </div>
+
+  <div class="form-row">
+    <div class="col-12">
+      <& /Elements/Submit, Label => loc('Save Changes'), Name => 'Update' &>
+    </div>
+  </div>
+
+</form>
+
+<%INIT>
+Abort("No form id found") unless $id;
+
+my $form_attribute = RT::Attribute->new($session{'CurrentUser'});
+my ($ok, $msg) = $form_attribute->Load($id);
+
+unless ( $ok ) {
+    Abort("Unable to load form with id $id");
+}
+
+my $content = $form_attribute->Content;
+my $encoded_content = JSON::to_json($content, { canonical => 1, pretty => 1 } );
+
+my ($title, @results);
+$title = loc("Modify form [_1]", $form_attribute->Description);
+
+if ( $Update ) {
+    Abort( loc('Permission Denied') )
+        unless $session{'CurrentUser'}->HasRight( Object => $RT::System, Right => 'SuperUser' );
+
+    if ( $ARGS{Description} ) {
+
+        if ( $ARGS{Description} ne $form_attribute->Description ) {
+            my $attr = RT::Attribute->new( RT->SystemUser );
+            my ($ret) = $attr->LoadByCols( Name => 'FormTools Form', Description => $ARGS{Description} );
+            if ($ret) {
+                push @results, loc( 'Name [_1] already exists', $ARGS{Description} );
+            }
+            else {
+                my ( $ret, $msg ) = $form_attribute->SetDescription( $ARGS{Description} );
+                if ($ret) {
+                    push @results, loc('Updated name');
+                }
+                else {
+                    push @results, loc( "Couldn't update name: [_1]", $msg );
+                }
+            }
+        }
+    }
+    else {
+        push @results, loc('Missing Name');
+    }
+
+    my $new_content = eval { JSON::from_json( $ARGS{Content} ) };
+    if ( $@ ) {
+        push @results, loc( "Couldn't decode JSON" );
+    }
+    # because of different line endings, $encoded_content and submitted
+    # $ARGS{Content} could differ even user doesn't change any thing.
+    elsif ( $form_attribute->_SerializeContent($new_content) ne $form_attribute->_SerializeContent($content) ) {
+        my ( $ret, $msg ) = $form_attribute->SetContent($new_content);
+        if ($ret) {
+            push @results, loc('Updated content');
+        }
+        else {
+            push @results, loc( "Couldn't update content: [_1]", $msg );
+        }
+    }
+
+    MaybeRedirectForResults(
+        Actions   => \@results,
+        Path      => '/Admin/FormTools/Advanced.html',
+        Arguments => { id => $id, Content => $ARGS{Content} },
+    );
+}
+</%INIT>
+<%ARGS>
+$id     => undef
+$Update => undef
+</%ARGS>
diff --git a/html/Callbacks/FormTools/Elements/Tabs/Privileged b/html/Callbacks/FormTools/Elements/Tabs/Privileged
index 24ad687..ab2b30c 100644
--- a/html/Callbacks/FormTools/Elements/Tabs/Privileged
+++ b/html/Callbacks/FormTools/Elements/Tabs/Privileged
@@ -26,6 +26,7 @@ if ( $m->request_path =~ m{^/Admin/FormTools/} ) {
 
         $page->child( modify => title => loc('Modify'), path => "/Admin/FormTools/Modify.html?id=" . $id );
         $page->child( description => title => loc('Description'), path => "/Admin/FormTools/Describe.html?id=" . $id );
+        $page->child( advanced => title => loc('Advanced'), path => "/Admin/FormTools/Advanced.html?id=" . $id );
     }
 }
 

commit 733ff241165aa8945bac56918ec04ad7276d651e
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Fri Sep 22 17:01:55 2023 -0400

    Support to create forms via web UI

diff --git a/html/Admin/FormTools/Create.html b/html/Admin/FormTools/Create.html
new file mode 100644
index 0000000..cbe5b7a
--- /dev/null
+++ b/html/Admin/FormTools/Create.html
@@ -0,0 +1,81 @@
+<& /Admin/Elements/Header, Title => $title &>
+<& /Elements/Tabs &>
+<& /Elements/ListActions, actions => \@results &>
+
+<form action="<%RT->Config->Get('WebPath')%>/Admin/FormTools/Create.html" name="CreateFormTools" method="post" enctype="multipart/form-data" class="mx-auto max-width-sm">
+  <div class="form-row">
+    <div class="col-3 label">
+      <&|/l&>Name</&>:
+    </div>
+    <div class="col-9 value">
+      <input name="Description" class="form-control" value="<% $ARGS{Description} // '' %>"/>
+    </div>
+  </div>
+  <div class="form-row">
+    <div class="col-3 label">
+      <span class="prev-icon-helper"><&|/l&>Queue</&>:</span>\
+      <span class="far fa-question-circle icon-helper" data-toggle="tooltip" data-placement="top" data-original-title="<% loc("Tickets will be created in this queue") %>"></span>
+    </div>
+    <div class="col-9 value">
+      <& /Elements/SelectQueue, Name => 'Queue', Default => $ARGS{Queue} &>
+    </div>
+  </div>
+
+  <div class="form-row">
+    <div class="col-12">
+      <& /Elements/Submit, Label => loc('Create'), Name => 'Create' &>
+    </div>
+  </div>
+
+</form>
+
+<%INIT>
+Abort( loc('Permission Denied') )
+    unless $session{'CurrentUser'}->HasRight( Object => $RT::System, Right => 'SuperUser' );
+
+my ($title, @results);
+$title = loc('Create form');
+
+if ( $Create ) {
+    push @results, loc('Missing Name') unless $Description;
+    push @results, loc('Missing Queue') unless $Queue;
+
+    if ( $Description && $Queue ) {
+        my $form = RT::Attribute->new( RT->SystemUser );
+        my ($ret) = $form->LoadByCols( Name => 'FormTools Form', Description => $Description );
+        if ($ret) {
+            push @results, loc( 'Name [_1] already exists', $Description );
+        }
+        else {
+            my ( $ret, $msg ) = $form->Create(
+                Name        => 'FormTools Form',
+                Description => $ARGS{Description},
+                Object      => RT->System,
+                Content     => { queue => $ARGS{Queue} },
+            );
+
+            if ($ret) {
+                MaybeRedirectForResults(
+                    Actions   => [ loc('Created form [_1]', $Description) ],
+                    Path      => '/Admin/FormTools/Modify.html',
+                    Arguments => { id => $form->Id },
+                );
+            }
+            else {
+                push @results, loc( "Couldn't create the form: [_1]", $msg );
+            }
+        }
+    }
+
+    MaybeRedirectForResults(
+        Actions   => \@results,
+        Path      => '/Admin/FormTools/Create.html',
+        Arguments => { Description => $Description, Queue => $Queue },
+    );
+}
+</%INIT>
+<%ARGS>
+$Description => undef
+$Queue       => undef
+$Create      => undef
+</%ARGS>
diff --git a/html/Callbacks/FormTools/Elements/Tabs/Privileged b/html/Callbacks/FormTools/Elements/Tabs/Privileged
index 4fff02e..24ad687 100644
--- a/html/Callbacks/FormTools/Elements/Tabs/Privileged
+++ b/html/Callbacks/FormTools/Elements/Tabs/Privileged
@@ -11,6 +11,10 @@ if ( $session{'CurrentUser'}->HasRight( Object => RT->System, Right => 'SuperUse
 }
 
 if ( $m->request_path =~ m{^/Admin/FormTools/} ) {
+    my $page = PageMenu();
+    $page->child( select => title => loc('Select'), path => "/Admin/FormTools/index.html" );
+    $page->child( create => title => loc('Create'), path => "/Admin/FormTools/Create.html" );
+
     if ( ( $HTML::Mason::Commands::DECODED_ARGS->{'id'} || '' ) =~ /^(\d+)$/ ) {
         my $id  = $1;
         my $form_attribute = RT::Attribute->new($session{'CurrentUser'});
@@ -20,8 +24,6 @@ if ( $m->request_path =~ m{^/Admin/FormTools/} ) {
             RT->Logger->error("Unable to load form with id $id");
         }
 
-        my $page = PageMenu();
-        $page->child( select => title => loc('Select'), path => "/Admin/FormTools/index.html?id=" . $id );
         $page->child( modify => title => loc('Modify'), path => "/Admin/FormTools/Modify.html?id=" . $id );
         $page->child( description => title => loc('Description'), path => "/Admin/FormTools/Describe.html?id=" . $id );
     }

commit 99584f1d93e40808302e43fc70590ea0805d07a1
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Fri Sep 22 16:35:54 2023 -0400

    Initial version of self service Form home page

diff --git a/html/Admin/FormTools/Describe.html b/html/Admin/FormTools/Describe.html
new file mode 100644
index 0000000..4267f0c
--- /dev/null
+++ b/html/Admin/FormTools/Describe.html
@@ -0,0 +1,62 @@
+<& /Admin/Elements/Header, Title => $title &>
+<& /Elements/Tabs &>
+<& /Elements/ListActions, actions => \@results &>
+
+<&| /Widgets/TitleBox, title => '' &>
+  <p>Forms are shown to users on a page in the RT Self Service interface. Below you can manage how the form details will appear to end users.</p>
+  <form name="EditFormDescription" action="" method="post" enctype="multipart/form-data">
+  <input type="hidden" class="hidden" name="id" value="<% $id %>" />
+  <&| /Widgets/TitleBox, title => loc('Icon') &>
+    <p>Form to upload an icon</p>
+  </&>
+  <&| /Widgets/TitleBox, title => loc('Description') &>
+    <p>Describe what the form should be used for and include instructions to help users pick the correct form.</p>
+    <p>An HTML edit box for instructions</p>
+      <div class="form-row">
+        <div class="col-12">
+          <textarea autocomplete="off" class="form-control messagebox richtext" cols="80" rows="15" name="FormDescription"><%$form->{'form-description'}%></textarea>
+        </div>
+      </div>
+  </&>
+</&>
+<div class="form-row">
+  <div class="col-12">
+    <& /Elements/Submit, Label => loc('Save'), Name => 'SubmitDescription' &>
+  </div>
+</div>
+</form>
+<%init>
+
+# Handle id getting submitted twice and becoming an array
+my @id = ( ref $id eq 'ARRAY' ) ? @{$id} : ($id);
+$id = $id[0];
+Abort("No form id found") unless $id;
+
+my $form_attribute = RT::Attribute->new($session{'CurrentUser'});
+my ($ok, $msg) = $form_attribute->Load($id);
+
+unless ( $ok ) {
+    Abort("Unable to load form with id $id");
+}
+
+my $form = $form_attribute->Content;
+
+my ($title, @results);
+$title = loc("Description for form [_1]", $form_attribute->Description);
+
+if ( $ARGS{'SubmitDescription'} && $ARGS{'SubmitDescription'} eq 'Save' ) {
+    $form->{'form-description'} = $ARGS{'FormDescription'};
+
+
+    $form_attribute->SetContent($form);
+}
+
+MaybeRedirectForResults(
+    Actions   => \@results,
+    Arguments => { id => $id },
+);
+
+</%init>
+<%args>
+$id => undef
+</%args>
diff --git a/html/Callbacks/FormTools/Elements/Tabs/Privileged b/html/Callbacks/FormTools/Elements/Tabs/Privileged
index 5c39b0b..4fff02e 100644
--- a/html/Callbacks/FormTools/Elements/Tabs/Privileged
+++ b/html/Callbacks/FormTools/Elements/Tabs/Privileged
@@ -11,8 +11,20 @@ if ( $session{'CurrentUser'}->HasRight( Object => RT->System, Right => 'SuperUse
 }
 
 if ( $m->request_path =~ m{^/Admin/FormTools/} ) {
-    my $page = PageMenu();
-    $page->child( select => title => loc('Select'), path => "/Admin/FormTools/" );
+    if ( ( $HTML::Mason::Commands::DECODED_ARGS->{'id'} || '' ) =~ /^(\d+)$/ ) {
+        my $id  = $1;
+        my $form_attribute = RT::Attribute->new($session{'CurrentUser'});
+        my ($ok, $msg) = $form_attribute->Load($id);
+
+        unless ( $ok ) {
+            RT->Logger->error("Unable to load form with id $id");
+        }
+
+        my $page = PageMenu();
+        $page->child( select => title => loc('Select'), path => "/Admin/FormTools/index.html?id=" . $id );
+        $page->child( modify => title => loc('Modify'), path => "/Admin/FormTools/Modify.html?id=" . $id );
+        $page->child( description => title => loc('Description'), path => "/Admin/FormTools/Describe.html?id=" . $id );
+    }
 }
 
 </%init>
diff --git a/html/Callbacks/FormTools/Elements/Tabs/SelfService b/html/Callbacks/FormTools/Elements/Tabs/SelfService
new file mode 100644
index 0000000..35e70ab
--- /dev/null
+++ b/html/Callbacks/FormTools/Elements/Tabs/SelfService
@@ -0,0 +1,11 @@
+<%init>
+
+my $formtools = Menu->child('home')->child(
+    formtools => title => loc('Forms'),
+    path      => '/SelfService/Forms.html',
+);
+
+</%init>
+<%args>
+$Path
+</%args>
diff --git a/html/Elements/Forms/ListForms b/html/Elements/Forms/ListForms
new file mode 100644
index 0000000..63dc6e2
--- /dev/null
+++ b/html/Elements/Forms/ListForms
@@ -0,0 +1,19 @@
+<&|/Widgets/TitleBox, title => '', class => 'fullwidth' &>
+% while ( my $form_attribute = $forms->Next ) {
+%    my $form = $form_attribute->Content;
+<div class="row border border-primary">
+  <div class="col-4 text-center">
+    <p><% $form_attribute->Description %></p>
+  </div>
+  <div class="col-8 text-left">
+    <% $form->{'form-description'} %>
+  </div>
+</div>
+% }
+</&>
+<%init>
+my $forms = RT::Attributes->new( RT->SystemUser );
+$forms->Limit( FIELD => 'Name', VALUE => 'FormTools Form' );
+</%init>
+<%args>
+</%args>
diff --git a/html/SelfService/Forms.html b/html/SelfService/Forms.html
new file mode 100644
index 0000000..9932bbc
--- /dev/null
+++ b/html/SelfService/Forms.html
@@ -0,0 +1,4 @@
+
+<& /SelfService/Elements/Header, Title => loc('Forms Home') &>
+
+<& /Elements/Forms/ListForms &>

commit b962fdd43039d9b29d2eb3692f48d8640bdd7843
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Wed Sep 20 16:53:46 2023 -0400

    Add basic drag and drop

diff --git a/META.yml b/META.yml
index 470eb1d..9f86295 100644
--- a/META.yml
+++ b/META.yml
@@ -19,6 +19,7 @@ no_index:
     - etc
     - html
     - inc
+    - static
 requires:
   perl: 5.10.1
 resources:
diff --git a/html/Admin/FormTools/Modify.html b/html/Admin/FormTools/Modify.html
index 146d4a2..95e40dd 100644
--- a/html/Admin/FormTools/Modify.html
+++ b/html/Admin/FormTools/Modify.html
@@ -6,23 +6,18 @@
 <div class="row">
   <div class="formtools-component-menu boxcontainer col-md-3" id="formtools-component-wrapper">
     <&| /Widgets/TitleBox, title => loc('FormTools Components') &>
-      <div aria-label="FormTools HTML Components">
-        <button type="button" class="btn btn-outline-primary btn-block">H1 Element</button>
-        <button type="button" class="btn btn-outline-primary btn-block">H2 Element</button>
-        <button type="button" class="btn btn-outline-primary btn-block">HR Element</button>
-      </div>
+      <div class="d-block text-center">
+% foreach my $item ( @html_components ) {
+    <p id="formtools-element-<% $item %>" class="m-1 p-2 border border-primary rounded" draggable="true" ondragstart="dragstart_handler(event);" ondragend="dragend_handler(event);"><% uc($item) %> Element</p>
+% }
       <hr />
-      <div aria-label="FormTools RT Core Components">
-        <button type="button" class="btn btn-outline-primary btn-block">Requestors</button>
-        <button type="button" class="btn btn-outline-primary btn-block">Owner</button>
-        <button type="button" class="btn btn-outline-primary btn-block">Subject</button>
-        <button type="button" class="btn btn-outline-primary btn-block">Content</button>
-      </div>
+% foreach my $item ( @core_components ) {
+    <p id="formtools-element-<% $item %>" class="m-1 p-2 border border-primary rounded" draggable="true" ondragstart="dragstart_handler(event);" ondragend="dragend_handler(event);"><% ucfirst($item) %></p>
+% }
       <hr />
-      <div aria-label="FormTools RT Custom Fields">
-        <button type="button" class="btn btn-outline-primary btn-block">Custom Field 1</button>
-        <button type="button" class="btn btn-outline-primary btn-block">Custom Field 2</button>
-        <button type="button" class="btn btn-outline-primary btn-block">Custom Field 3</button>
+% foreach my $item ( @custom_fields ) {
+    <p id="formtools-element-<% $item %>" class="m-1 p-2 border border-primary rounded" draggable="true" ondragstart="dragstart_handler(event);" ondragend="dragend_handler(event);"><% uc($item) %></p>
+% }
       </div>
     </&>
   </div>
@@ -43,6 +38,9 @@
     </li>
 % }
   </ul>
+  <p>Drag components from the left toolbar and drop them here</p>
+    <div id="formtools-content-form-1" class="formtools-content w-100 border border-primary rounded" ondrop="drop_handler(event);" ondragover="dragover_handler(event);" ondragend="dragend_handler(event);">
+    </div>
 </&>
 </div><!-- row -->
 </div><!-- formtools-form-pages -->
@@ -73,6 +71,10 @@ $title = loc("Modify form [_1]", $form_attribute->Description);
 
 my $nav_type = 'pill'; # 'tab' or 'pill'
 
+my @html_components = qw( h1 h2 h3 hr );
+my @core_components = qw( requestors owner subject content );
+my @custom_fields = qw( cf1 cf2 cf3 );
+
 </%INIT>
 <%ARGS>
 $id => undef
diff --git a/lib/RT/Extension/FormTools.pm b/lib/RT/Extension/FormTools.pm
index 8ff77d5..c1dbd38 100644
--- a/lib/RT/Extension/FormTools.pm
+++ b/lib/RT/Extension/FormTools.pm
@@ -5,6 +5,9 @@ package RT::Extension::FormTools;
 
 our $VERSION = '0.53';
 
+RT->AddStyleSheets('rt-extension-formtools.css');
+RT->AddJavaScript('rt-extension-formtools.js');
+
 =head1 NAME
 
 RT-Extension-FormTools - Help write multi-page ticket creation wizards
diff --git a/static/css/rt-extension-formtools.css b/static/css/rt-extension-formtools.css
new file mode 100644
index 0000000..ad60315
--- /dev/null
+++ b/static/css/rt-extension-formtools.css
@@ -0,0 +1,4 @@
+
+div .formtools-content {
+    min-height: 400px;
+}
diff --git a/static/js/rt-extension-formtools.js b/static/js/rt-extension-formtools.js
new file mode 100644
index 0000000..31c5d6e
--- /dev/null
+++ b/static/js/rt-extension-formtools.js
@@ -0,0 +1,69 @@
+function dragstart_handler(ev) {
+  console.log("dragStart");
+  var dti = ev.dataTransfer.items;
+  if (dti === undefined || dti == null) {
+    console.log("Browser does not support DataTransferItem interface");
+    return;
+  }
+
+  // Add the id of the drag source element to the drag data payload so
+  // it is available when the drop event is fired
+  dti.add(ev.target.id, "text/plain");
+  // Tell the browser both copy and move are possible
+  ev.effectAllowed = "copy";
+}
+
+function dragover_handler(ev) {
+    console.log("dragOver");
+    var dti = ev.dataTransfer.items;
+    if (dti === undefined || dti == null) {
+        console.log("Browser does not support DataTransferItem interface");
+        return;
+    }
+    // Change the target element's border to signify a drag over event
+    // has occurred
+    ev.currentTarget.style.background = "lightgray";
+    ev.preventDefault();
+}
+
+function drop_handler(ev) {
+    console.log("Drop");
+    ev.preventDefault();
+    var dti = ev.dataTransfer.items;
+    if (dti === undefined || dti == null) {
+        console.log("Browser does not support DataTransferItem interface");
+        return;
+    }
+    // Get the id of the drag source element (that was added to the drag data
+    // payload by the dragstart event handler). Even though only one drag item
+    // was explicitly added, the browser may include other items so need to search
+    // for the plain/text item.
+    for (var i=0; i < dti.length; i++) {
+        console.log("Drop: item[" + i + "].kind = " + dti[i].kind + " ; item[" + i + "].type = " + dti[i].type);
+        if ((dti[i].kind == 'string') && (dti[i].type.match('^text/plain'))) {
+          // This item is the target node
+          dti[i].getAsString(function (id){
+              // Copy the element
+              var nodeCopy = document.getElementById(id).cloneNode(true);
+              console.log("Copying " + nodeCopy);
+              nodeCopy.id = "newId";
+              ev.target.appendChild(nodeCopy);
+          });
+        }
+    }
+
+    // Clear background
+    ev.currentTarget.style.background = "none";
+}
+
+function dragend_handler(ev) {
+    console.log("dragEnd");
+    var dti = ev.dataTransfer.items;
+    if (dti === undefined || dti == null) {
+        console.log("Browser does not support DataTransferItem interface");
+        return;
+    }
+
+    // Remove all of the items from the list.
+    dti.clear();
+}

commit 422f6a6bcb2754f64f38c193e8a6c8731f9b1a74
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Tue Sep 19 17:09:00 2023 -0400

    Basic component layout

diff --git a/html/Admin/FormTools/Modify.html b/html/Admin/FormTools/Modify.html
index 479614a..146d4a2 100644
--- a/html/Admin/FormTools/Modify.html
+++ b/html/Admin/FormTools/Modify.html
@@ -4,11 +4,28 @@
 <div id="formtools-edit">
 <& /Admin/Elements/FormToolsHelp &>
 <div class="row">
-<div class="formtools-component-menu boxcontainer col-md-3" id="formtools-component-wrapper">
-<&| /Widgets/TitleBox, title => loc('FormTools Components') &>
-<p>FormTools help goes here.</p>
-</&>
-</div>
+  <div class="formtools-component-menu boxcontainer col-md-3" id="formtools-component-wrapper">
+    <&| /Widgets/TitleBox, title => loc('FormTools Components') &>
+      <div aria-label="FormTools HTML Components">
+        <button type="button" class="btn btn-outline-primary btn-block">H1 Element</button>
+        <button type="button" class="btn btn-outline-primary btn-block">H2 Element</button>
+        <button type="button" class="btn btn-outline-primary btn-block">HR Element</button>
+      </div>
+      <hr />
+      <div aria-label="FormTools RT Core Components">
+        <button type="button" class="btn btn-outline-primary btn-block">Requestors</button>
+        <button type="button" class="btn btn-outline-primary btn-block">Owner</button>
+        <button type="button" class="btn btn-outline-primary btn-block">Subject</button>
+        <button type="button" class="btn btn-outline-primary btn-block">Content</button>
+      </div>
+      <hr />
+      <div aria-label="FormTools RT Custom Fields">
+        <button type="button" class="btn btn-outline-primary btn-block">Custom Field 1</button>
+        <button type="button" class="btn btn-outline-primary btn-block">Custom Field 2</button>
+        <button type="button" class="btn btn-outline-primary btn-block">Custom Field 3</button>
+      </div>
+    </&>
+  </div>
 <div class="formtools-form-pages boxcontainer col-md-9" id="formtools-pages-wrapper">
 <&| /Widgets/TitleBox, title => loc('FormTools Pages') &>
 % my @form_pages = keys %{$form->{'formtools-pages'}};

commit e649e992f3c14fae17c00196b452ed0ccb8cd6e8
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Tue Sep 19 16:49:44 2023 -0400

    Add menus

diff --git a/html/Callbacks/FormTools/Elements/Tabs/Privileged b/html/Callbacks/FormTools/Elements/Tabs/Privileged
new file mode 100644
index 0000000..5c39b0b
--- /dev/null
+++ b/html/Callbacks/FormTools/Elements/Tabs/Privileged
@@ -0,0 +1,22 @@
+<%init>
+
+if ( $session{'CurrentUser'}->HasRight( Object => RT->System, Right => 'SuperUser' ) ) {
+    my $formtools = Menu->child('admin')->child(
+        formtools => title => loc('FormTools'),
+        path      => '/Admin/FormTools/',
+    );
+
+    $formtools->child( select => title => loc('Select'), path => '/Admin/FormTools/' );
+    $formtools->child( create => title => loc('Create'), path => '/Admin/FormTools/Create.html' );
+}
+
+if ( $m->request_path =~ m{^/Admin/FormTools/} ) {
+    my $page = PageMenu();
+    $page->child( select => title => loc('Select'), path => "/Admin/FormTools/" );
+}
+
+</%init>
+<%args>
+$Path
+$Has_Query
+</%args>

commit ff6d0ac06a7adfbbfc618e08460f647af2807926
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Mon Sep 18 16:01:19 2023 -0400

    Move all form pages into a defined entry in config
    
    This makes it possible to separate form metadata from
    page configurations.

diff --git a/etc/sample_form.json b/etc/sample_form.json
index e716882..8987269 100644
--- a/etc/sample_form.json
+++ b/etc/sample_form.json
@@ -1,108 +1,110 @@
 {
-   "submit" : {
-      "content" : [
-         {
-            "html" : "<h1>Account Request</h1>",
-            "type" : "raw_html"
-         },
-         {
-            "type" : "raw_html",
-            "html" : "<h2>Request Submitted</h2>"
-         },
-         {
-            "type" : "raw_html",
-            "html" : "<p>Your request has been submitted.</p>"
-         }
-      ],
-      "name" : "Submit Request",
-      "next" : ""
+   "formtools-pages" : {
+       "submit" : {
+          "content" : [
+             {
+                "html" : "<h1>Account Request</h1>",
+                "type" : "raw_html"
+             },
+             {
+                "type" : "raw_html",
+                "html" : "<h2>Request Submitted</h2>"
+             },
+             {
+                "type" : "raw_html",
+                "html" : "<p>Your request has been submitted.</p>"
+             }
+          ],
+          "name" : "Submit Request",
+          "next" : ""
+       },
+       "page2" : {
+          "validation" : 1,
+          "name" : "Page Two",
+          "content" : [
+             {
+                "html" : "<h1>Account Request</h1>",
+                "type" : "raw_html"
+             },
+             {
+                "comp_name" : "Field",
+                "arguments" : {
+                   "name" : "Requestors",
+                   "label" : "Requested by",
+                   "default" : "root at localhost"
+                },
+                "type" : "component"
+             },
+             {
+                "arguments" : {
+                   "name" : "Systems"
+                },
+                "type" : "component",
+                "comp_name" : "Field"
+             }
+          ],
+          "next" : "page3"
+       },
+       "page3" : {
+          "content" : [
+             {
+                "type" : "raw_html",
+                "html" : "<h1>Account Request</h1>"
+             },
+             {
+                "type" : "raw_html",
+                "html" : "<h2>Confirm Selections</h2>"
+             },
+             {
+                "type" : "component",
+                "comp_name" : "ShowChoices"
+             },
+             {
+                "type" : "raw_html",
+                "html" : "<hr/>\n<i><b>\n<p>What else should we know as we act on your request?</p>\n<p> Do you have special time constraints?</p>\n</b></i>"
+             },
+             {
+                "comp_name" : "Field",
+                "arguments" : {
+                   "name" : "Content"
+                },
+                "type" : "component"
+             },
+             {
+                "input-value" : "create_ticket",
+                "type" : "hidden",
+                "input-name" : "create_ticket"
+             }
+          ],
+          "name" : "Review Selections",
+          "validation" : 1,
+          "next" : "submit"
+       },
+       "page1" : {
+          "validation" : 1,
+          "name" : "Page One",
+          "content" : [
+             {
+                "html" : "<h1>Account Request</h1>",
+                "type" : "raw_html"
+             },
+             {
+                "type" : "raw_html",
+                "html" : "<h2>Enter your preferred username</h2>"
+             },
+             {
+                "comp_name" : "Field",
+                "type" : "component",
+                "arguments" : {
+                   "default" : "preferred username",
+                   "show_validation" : 1,
+                   "name" : "Username"
+                }
+             }
+          ],
+          "next" : "page2"
+       }
    },
    "queue" : 1,
-   "page2" : {
-      "validation" : 1,
-      "name" : "Page Two",
-      "content" : [
-         {
-            "html" : "<h1>Account Request</h1>",
-            "type" : "raw_html"
-         },
-         {
-            "comp_name" : "Field",
-            "arguments" : {
-               "name" : "Requestors",
-               "label" : "Requested by",
-               "default" : "root at localhost"
-            },
-            "type" : "component"
-         },
-         {
-            "arguments" : {
-               "name" : "Systems"
-            },
-            "type" : "component",
-            "comp_name" : "Field"
-         }
-      ],
-      "next" : "page3"
-   },
-   "formtools-start-page" : "page1",
-   "page3" : {
-      "content" : [
-         {
-            "type" : "raw_html",
-            "html" : "<h1>Account Request</h1>"
-         },
-         {
-            "type" : "raw_html",
-            "html" : "<h2>Confirm Selections</h2>"
-         },
-         {
-            "type" : "component",
-            "comp_name" : "ShowChoices"
-         },
-         {
-            "type" : "raw_html",
-            "html" : "<hr/>\n<i><b>\n<p>What else should we know as we act on your request?</p>\n<p> Do you have special time constraints?</p>\n</b></i>"
-         },
-         {
-            "comp_name" : "Field",
-            "arguments" : {
-               "name" : "Content"
-            },
-            "type" : "component"
-         },
-         {
-            "input-value" : "create_ticket",
-            "type" : "hidden",
-            "input-name" : "create_ticket"
-         }
-      ],
-      "name" : "Review Selections",
-      "validation" : 1,
-      "next" : "submit"
-   },
-   "page1" : {
-      "validation" : 1,
-      "name" : "Page One",
-      "content" : [
-         {
-            "html" : "<h1>Account Request</h1>",
-            "type" : "raw_html"
-         },
-         {
-            "type" : "raw_html",
-            "html" : "<h2>Enter your preferred username</h2>"
-         },
-         {
-            "comp_name" : "Field",
-            "type" : "component",
-            "arguments" : {
-               "default" : "preferred username",
-               "show_validation" : 1,
-               "name" : "Username"
-            }
-         }
-      ],
-      "next" : "page2"
-   }
+   "formtools-start-page" : "page1"
 }
diff --git a/html/Admin/FormTools/Modify.html b/html/Admin/FormTools/Modify.html
index 9e0ae3d..479614a 100644
--- a/html/Admin/FormTools/Modify.html
+++ b/html/Admin/FormTools/Modify.html
@@ -11,7 +11,7 @@
 </div>
 <div class="formtools-form-pages boxcontainer col-md-9" id="formtools-pages-wrapper">
 <&| /Widgets/TitleBox, title => loc('FormTools Pages') &>
-% my @form_pages = keys %$form;
+% my @form_pages = keys %{$form->{'formtools-pages'}};
   <ul class="nav nav-<% $nav_type %>s" id="formtools-pages">
 % my $current_context = {};
 % foreach my $page_name (@form_pages) {
diff --git a/html/Forms/dhandler b/html/Forms/dhandler
index 242c143..790aece 100644
--- a/html/Forms/dhandler
+++ b/html/Forms/dhandler
@@ -1,5 +1,5 @@
-<&|/FormTools/Form, next => '/Forms/' . $form_name . '/' . $form_config->{$page}{'next'},
-    validation => $form_config->{$page}{'validation'},
+<&|/FormTools/Form, next => '/Forms/' . $form_name . '/' . $form_config->{'formtools-pages'}{$page}{'next'},
+    validation => $form_config->{'formtools-pages'}{$page}{'validation'},
     next_for_validation => '/Forms/' . $form_name . '/' . $page,
     results_ref => \@results,
 &>
@@ -7,7 +7,7 @@
 <%perl>
 # Build the current page here dyamically from config
 
-foreach my $element ( @{$form_config->{$page}{'content'}} ) {
+foreach my $element ( @{$form_config->{'formtools-pages'}{$page}{'content'}} ) {
     if ( $element->{type} eq 'raw_html' ) {
         $m->out( $element->{html} );
     }
@@ -81,7 +81,7 @@ unless ( $ok ) {
 }
 
 $m->notes( queue => $queue_obj );
-$m->notes( page_title => $form_config->{$page}{'name'} );
+$m->notes( page_title => $form_config->{'formtools-pages'}{$page}{'name'} );
 
 # Try to create a ticket if we're on the last page and
 # "create_ticket" is submitted as an arg from the second-to-last

commit 8ac4f6caf61aa8dc25c715b5f5a2b21f7cb948e3
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Fri Sep 15 17:03:18 2023 -0400

    Create initial admin pages

diff --git a/html/Admin/Elements/FormToolsHelp b/html/Admin/Elements/FormToolsHelp
new file mode 100644
index 0000000..2df8c64
--- /dev/null
+++ b/html/Admin/Elements/FormToolsHelp
@@ -0,0 +1,52 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2023 Best Practical Solutions, LLC
+%#                                          <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<div class="help" id="rt-help-text">
+<&| /Widgets/TitleBox, title => loc('FormTools Configuration') &>
+<p>FormTools help goes here.</p>
+</&>
+</div>
diff --git a/html/Admin/FormTools/Modify.html b/html/Admin/FormTools/Modify.html
new file mode 100644
index 0000000..9e0ae3d
--- /dev/null
+++ b/html/Admin/FormTools/Modify.html
@@ -0,0 +1,62 @@
+<& /Admin/Elements/Header, Title => $title &>
+<& /Elements/Tabs &>
+<& /Elements/ListActions, actions => \@results &>
+<div id="formtools-edit">
+<& /Admin/Elements/FormToolsHelp &>
+<div class="row">
+<div class="formtools-component-menu boxcontainer col-md-3" id="formtools-component-wrapper">
+<&| /Widgets/TitleBox, title => loc('FormTools Components') &>
+<p>FormTools help goes here.</p>
+</&>
+</div>
+<div class="formtools-form-pages boxcontainer col-md-9" id="formtools-pages-wrapper">
+<&| /Widgets/TitleBox, title => loc('FormTools Pages') &>
+% my @form_pages = keys %$form;
+  <ul class="nav nav-<% $nav_type %>s" id="formtools-pages">
+% my $current_context = {};
+% foreach my $page_name (@form_pages) {
+%     my $tab_id = CSSClass( $page_name );
+%     $current_context->{tab} = $tab_id;
+% #     my( $active, $aria_selected) = $tab_id eq $active_context->{tab} ? ('active', 'true') : ('', 'false');
+%     my( $active, $aria_selected) = ('active', 'true');
+%     my $nav_id = join '-', 'nav', $current_context->{tab};
+%     my $content_id = join '-', 'content', $current_context->{tab};
+    <li class="nav-item">
+      <a class="nav-link <% $active %>" id="<% $nav_id %>" data-toggle="<% $nav_type %>" href="#<% $content_id %>" role="<% $nav_type %>" aria-controls="<% $content_id %>" aria-selected="<% $aria_selected %>"><% $page_name %></a>
+    </li>
+% }
+  </ul>
+</&>
+</div><!-- row -->
+</div><!-- formtools-form-pages -->
+</div><!-- formtools-edit -->
+
+<p><&|/l&>Loaded form <% $form_attribute->Description %></&></p>
+
+<pre>
+% use Data::Printer;
+% $m->out(np($form));
+</pre>
+
+<%INIT>
+
+Abort("No form id found") unless $id;
+
+my $form_attribute = RT::Attribute->new($session{'CurrentUser'});
+my ($ok, $msg) = $form_attribute->Load($id);
+
+unless ( $ok ) {
+    Abort("Unable to load form with id $id");
+}
+
+my $form = $form_attribute->Content;
+
+my ($title, @results);
+$title = loc("Modify form [_1]", $form_attribute->Description);
+
+my $nav_type = 'pill'; # 'tab' or 'pill'
+
+</%INIT>
+<%ARGS>
+$id => undef
+</%ARGS>
diff --git a/html/Admin/FormTools/index.html b/html/Admin/FormTools/index.html
new file mode 100644
index 0000000..68f0913
--- /dev/null
+++ b/html/Admin/FormTools/index.html
@@ -0,0 +1,57 @@
+<& /Admin/Elements/Header, Title => loc("Admin FormTools") &>
+<& /Elements/Tabs &>
+<& /Elements/ListActions, actions => \@results &>
+
+  <table class="table collection collection-as-table" cellspacing="0">
+    <tbody>
+      <tr class="collection-as-table">
+        <th class="collection-as-table"><&|/l&>Name</&></th>
+        <th class="collection-as-table"><&|/l&>Creates Tickets in Queue</&></th>
+      </tr>
+% my $i = 0;
+% for my $form_ref ( @forms ) {
+
+      <tr class="<% $i % 2 ? 'oddline' : 'evenline' %>">
+        <td class="collection-as-table"><a href="<% RT->Config->Get('WebURL') %>Admin/FormTools/Modify.html?id=<% $form_ref->{'id'} %>"><% $form_ref->{'name'} %></a></td>
+        <td class="collection-as-table"><% $form_ref->{'queue_name'} %></td>
+      </tr>
+%    ++$i;
+% }
+    </tbody>
+  </table>
+
+<%init>
+my @results;
+
+my $forms = RT::Attributes->new( $session{'CurrentUser'} );
+$forms->Limit( FIELD => 'Name', VALUE => 'FormTools Form' );
+
+my @unsorted_forms;
+while ( my $form = $forms->Next ) {
+    my $form_ref = $form->Content;
+    $form_ref->{'name'} = $form->Description;
+    $form_ref->{'id'} = $form->Id;
+
+    my $queue = RT::Queue->new( $session{'CurrentUser'} );
+    my ($ok, $msg) = $queue->Load($form_ref->{'queue'});
+    if ( $ok ) {
+        $form_ref->{'queue_name'} = $queue->Name;
+    }
+    else {
+        RT->Logger->error("FormTools unable to load queue " . $form_ref->{'queue'} . " $msg");
+    }
+
+    push @unsorted_forms, $form_ref;
+}
+
+my @forms = sort { $a->{'name'} cmp $b->{'name'} } @unsorted_forms;
+
+$m->callback(
+    CallbackName     => 'Initial',
+    FormsRef         => \@forms,
+    ARGSRef          => \%ARGS
+);
+
+</%init>
+<%args>
+</%args>

commit 6e8a44fe07c82304f36b080197c6c7be67a1ddb9
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Fri Sep 8 14:47:25 2023 -0400

    Create tickets when create_ticket is passed
    
    The final result of any form wizard is to eventually
    create a ticket. Provide a standard way of doing
    that, passing in all accumulated arguments for
    values on that new ticket.

diff --git a/html/FormTools/Form b/html/FormTools/Form
index b5839eb..58418b6 100644
--- a/html/FormTools/Form
+++ b/html/FormTools/Form
@@ -12,6 +12,7 @@ $form_id             => undef
 $form_name           => undef
 $form_classes        => undef
 $self_service        => 0
+$results_ref         => []
 </%args>
 <%init>
 use RT::Extension::FormTools;
@@ -81,6 +82,10 @@ foreach my $key (keys %request_args) {
    delete $request_args{$key} if ($core_fields{$key});
 }
 
+# Add in any @results passed in. These should not be errors,
+# so they will be shown on the next page.
+push @results, @{$results_ref};
+
 $next_for_validation ||= $m->caller(1)->path;
 
 </%init>
diff --git a/html/Forms/dhandler b/html/Forms/dhandler
index eb2a0eb..242c143 100644
--- a/html/Forms/dhandler
+++ b/html/Forms/dhandler
@@ -1,6 +1,7 @@
 <&|/FormTools/Form, next => '/Forms/' . $form_name . '/' . $form_config->{$page}{'next'},
     validation => $form_config->{$page}{'validation'},
     next_for_validation => '/Forms/' . $form_name . '/' . $page,
+    results_ref => \@results,
 &>
 
 <%perl>
@@ -82,7 +83,23 @@ unless ( $ok ) {
 $m->notes( queue => $queue_obj );
 $m->notes( page_title => $form_config->{$page}{'name'} );
 
+# Try to create a ticket if we're on the last page and
+# "create_ticket" is submitted as an arg from the second-to-last
+# page.
+
+my ($ticket_obj, @results);
+if ( $create_ticket ) {
+    ($ticket_obj, @results) = CreateTicket(
+        Subject => 'Ticket created from FormTools form ' . $form_name,
+        Queue   => $queue_obj->Id,
+        Status => 'new',
+        Requestors => $session{'CurrentUser'}->EmailAddress,
+        %ARGS,
+    );
+}
+
 </%init>
 <%args>
 $_form_tools_next => undef
+$create_ticket => undef
 </%args>

commit f48884ee2b567433d3c288b7e021e67af5ecd8d6
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Fri Sep 8 14:41:31 2023 -0400

    Align validation hint handling with RT updates
    
    RT now allows custom messages as validation hints.

diff --git a/html/FormTools/Field b/html/FormTools/Field
index bc74274..ca20a17 100644
--- a/html/FormTools/Field
+++ b/html/FormTools/Field
@@ -192,9 +192,13 @@ $default = '' unless defined $default;
             QueueObj => $queue,
             ($default ? (Default => $default) : ())
             &>
-% if ($show_validation && $cf->FriendlyPattern) {
-    <span class="cfhints"><% loc("Input must match [_1]", $cf->FriendlyPattern) %></span>
-%}
+%      if (my $msg = $m->notes('InvalidField-' . $cf->Id)) {
+            <span class="cfinvalidfield my-1 d-inline-block"><% $msg %></span>
+%      } elsif ($show_validation and $cf->FriendlyPattern) {
+            <span class="cfhints my-1 d-inline-block">
+              <% $cf->FriendlyPattern %>
+            </span>
+%      }
 % } elsif ($render_as =~  /^boolean/i)  {
 %       my $value = 'Yes';
 <div class="custom-control custom-checkbox" style="margin-top: 5px">

commit 9ef88c4a6d6da87bc35d052b4a911f500433369d
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Fri Sep 8 14:38:42 2023 -0400

    Replace custom validation with standard RT version

diff --git a/html/FormTools/Form b/html/FormTools/Form
index 665df7b..b5839eb 100644
--- a/html/FormTools/Form
+++ b/html/FormTools/Form
@@ -53,18 +53,15 @@ if ($validation && $real_next) {
         }
     }
 
-    # cf validation based on record pattern
-    foreach my $key (keys %request_args) {
-        next if $key =~ /Values-Magic$/;
-        next unless ($key =~ /CustomField-(\d+)/ );
-        my $id = $1;
-        my $cf = RT::CustomField->new($session{'CurrentUser'});
-        $cf->Load($id);
-        next unless exists $request_args{"Object-RT::Ticket--CustomField-@{[ $cf->Id ]}-Values-Magic"};
-        my ($ok, @res) = RT::Extension::FormTools::validate_cf($cf, \%request_args);
-        push @results, @res unless $ok;
-
+    my ($status, @msg) = $m->comp(
+        '/Elements/ValidateCustomFields',
+        CustomFields    => $queue->TicketCustomFields,
+        ARGSRef         => \%request_args,
+    );
+    unless ($status) {
+        push @results, @msg;
     }
+
     unless (@results) {
         $real_next = $m->caller(1)->dir_path . '/' . $real_next
             unless $real_next =~ m'^/';

commit fa89e10fe2e4cc2555d239f838a6a967c1b27185
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Fri Sep 8 14:50:34 2023 -0400

    Update support files for new utility

diff --git a/.gitignore b/.gitignore
index cff0b80..cc52a86 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,3 +12,4 @@ pod2htm*.tmp
 /MYMETA.*
 /t/tmp
 /xt/tmp
+/bin/rt-insert-formtools-config
diff --git a/inc/Module/Install/Substitute.pm b/inc/Module/Install/Substitute.pm
new file mode 100644
index 0000000..56af7fe
--- /dev/null
+++ b/inc/Module/Install/Substitute.pm
@@ -0,0 +1,131 @@
+#line 1
+package Module::Install::Substitute;
+
+use strict;
+use warnings;
+use 5.008; # I don't care much about earlier versions
+
+use Module::Install::Base;
+our @ISA = qw(Module::Install::Base);
+
+our $VERSION = '0.03';
+
+require File::Temp;
+require File::Spec;
+require Cwd;
+
+#line 89
+
+sub substitute
+{
+	my $self = shift;
+	$self->{__subst} = shift;
+	$self->{__option} = {};
+	if( UNIVERSAL::isa( $_[0], 'HASH' ) ) {
+		my $opts = shift;
+		while( my ($k,$v) = each( %$opts ) ) {
+			$self->{__option}->{ lc( $k ) } = $v || '';
+		}
+	}
+	$self->_parse_options;
+
+	my @file = @_;
+	foreach my $f (@file) {
+		$self->_rewrite_file( $f );
+	}
+
+	return;
+}
+
+sub _parse_options
+{
+	my $self = shift;
+	my $cwd = Cwd::getcwd();
+	foreach my $t ( qw(from to) ) {
+        $self->{__option}->{$t} = $cwd unless $self->{__option}->{$t};
+		my $d = $self->{__option}->{$t};
+		die "Couldn't read directory '$d'" unless -d $d && -r _;
+	}
+}
+
+sub _rewrite_file
+{
+	my ($self, $file) = @_;
+	my $source = File::Spec->catfile( $self->{__option}{from}, $file );
+	$source .= $self->{__option}{sufix} if $self->{__option}{sufix};
+	unless( -f $source && -r _ ) {
+		print STDERR "Couldn't find file '$source'\n";
+		return;
+	}
+	my $dest = File::Spec->catfile( $self->{__option}{to}, $file );
+	return $self->__rewrite_file( $source, $dest );
+}
+
+sub __rewrite_file
+{
+	my ($self, $source, $dest) = @_;
+
+	my $mode = (stat($source))[2];
+
+	open my $sfh, "<$source" or die "Couldn't open '$source' for read";
+	print "Open input '$source' file for substitution\n";
+
+	my ($tmpfh, $tmpfname) = File::Temp::tempfile('mi-subst-XXXX', UNLINK => 1);
+	$self->__process_streams( $sfh, $tmpfh, ($source eq $dest)? 1: 0 );
+	close $sfh;
+
+	seek $tmpfh, 0, 0 or die "Couldn't seek in tmp file";
+
+	open my $dfh, ">$dest" or die "Couldn't open '$dest' for write";
+	print "Open output '$dest' file for substitution\n";
+
+	while( <$tmpfh> ) {
+		print $dfh $_;
+	}
+	close $dfh;
+	chmod $mode, $dest or "Couldn't change mode on '$dest'";
+}
+
+sub __process_streams
+{
+	my ($self, $in, $out, $replace) = @_;
+	
+	my @queue = ();
+	my $subst = $self->{'__subst'};
+	my $re_subst = join('|', map {"\Q$_"} keys %{ $subst } );
+
+	while( my $str = <$in> ) {
+		if( $str =~ /^###\s*(before|replace|after)\:\s?(.*)$/s ) {
+			my ($action, $nstr) = ($1,$2);
+			$nstr =~ s/\@($re_subst)\@/$subst->{$1}/ge;
+
+			die "Replace action is bad idea for situations when dest is equal to source"
+                if $replace && $action eq 'replace';
+			if( $action eq 'before' ) {
+				die "no line before 'before' action" unless @queue;
+				# overwrite prev line;
+				pop @queue;
+				push @queue, $nstr;
+				push @queue, $str;
+			} elsif( $action eq 'replace' ) {
+				push @queue, $nstr;
+			} elsif( $action eq 'after' ) {
+				push @queue, $str;
+				push @queue, $nstr;
+				# skip one line;
+				<$in>;
+			}
+		} else {
+			push @queue, $str;
+		}
+		while( @queue > 3 ) {
+			print $out shift(@queue);
+		}
+	}
+	while( scalar @queue ) {
+		print $out shift(@queue);
+	}
+}
+
+1;
+

commit 413f78b5e974ee18813fc830e5e1ac07d35a0a08
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Fri Sep 8 13:46:28 2023 -0400

    Add utility to insert a FormTools config from JSON
    
    Mostly for testing, but could be useful for automatically
    inserting pre-configured forms in new extensions.

diff --git a/META.yml b/META.yml
index 7cbfb2b..470eb1d 100644
--- a/META.yml
+++ b/META.yml
@@ -16,6 +16,7 @@ meta-spec:
 name: RT-Extension-FormTools
 no_index:
   directory:
+    - etc
     - html
     - inc
 requires:
diff --git a/Makefile.PL b/Makefile.PL
index 4d0e2ec..ed71e84 100644
--- a/Makefile.PL
+++ b/Makefile.PL
@@ -4,6 +4,30 @@ requires_rt('5.0.0');
 
 repository('https://github.com/bestpractical/rt-extension-formtools');
 
+my ($lp) = ($INC{'RT.pm'} =~ /^(.*)[\\\/]/);
+my $lib_path = join( ' ', "$RT::LocalPath/lib", $lp );
+my $bin_path = $RT::BinPath || "$RT::BasePath/bin" || "/opt/rt5/bin";
+
+# Straight from perldoc perlvar
+use Config;
+my $secure_perl_path = $Config{perlpath};
+if ($^O ne 'VMS') {
+    $secure_perl_path .= $Config{_exe}
+        unless $secure_perl_path =~ m/$Config{_exe}$/i;
+}
+
+substitute(
+           {
+        RT_LIB_PATH  => $lib_path,
+        RT_BIN_PATH  => $bin_path,
+        PERL         => $ENV{PERL} || $secure_perl_path,
+    },
+           {
+        sufix => '.in'
+    },
+           qw(bin/rt-insert-formtools-config),
+);
+
 sign();
 WriteAll();
 
diff --git a/bin/rt-insert-formtools-config.in b/bin/rt-insert-formtools-config.in
new file mode 100644
index 0000000..6cf3573
--- /dev/null
+++ b/bin/rt-insert-formtools-config.in
@@ -0,0 +1,84 @@
+#!/usr/bin/env perl
+### before: #!@PERL@
+
+use strict;
+use warnings;
+
+### after:     use lib qw(@RT_LIB_PATH@);
+use lib '/opt/rt5/local/lib /opt/rt5/lib';
+
+use RT::Interface::CLI  qw(Init);
+
+use JSON;
+use Getopt::Long;
+
+my %OPT = (
+    'form-name' => 1,
+);
+
+Init(
+    \%OPT,
+    "help|h",
+    "form-name=s",
+);
+
+Pod::Usage::pod2usage({verbose => 1}) if $OPT{help};
+Pod::Usage::pod2usage() unless @ARGV == 1;
+
+unless ( $OPT{'form-name'} ) {
+    die "form-name is required to name the new form";
+}
+
+my ($filename) = @ARGV;
+die "Not a file: $filename\n" unless -f $filename;
+die "Cannot read file: $filename\n" unless -r $filename;
+
+open(my $fh, "<", $filename)
+    or die "Can't read $filename: $!";
+
+my $json_input = do {local $/; <$fh>} if $fh;
+my $formtools_hash = decode_json($json_input);
+
+my $form = RT::Attribute->new( RT->SystemUser );
+my ( $ok, $msg ) = $form->Create(
+    Name    => 'FormTools Form',
+    Description => $OPT{'form-name'},
+    Object => RT->System,
+    Content => $formtools_hash,
+);
+
+if ( $ok ) {
+    print "Form \"" . $OPT{'form-name'} . "\" created.\n";
+}
+else {
+    print "Error: $msg\n";
+}
+
+__END__
+
+=head1 NAME
+
+rt-insert-formtools-config - Process a JSON FormTools configuration
+file and insert it into the RT database.
+
+=head1 SYNOPSIS
+
+    rt-insert-formtools-config --form-name="Form One" formtools-config.json
+
+=head1 DESCRIPTION
+
+This script accepts a file containing a JSON structure to define
+a set of FormTools forms. It will insert this configuration into
+the RT database.
+
+=head1 OPTIONS
+
+=over
+
+=item C<--form-name>
+
+The name of the form to be created.
+
+=back
+
+=cut
diff --git a/etc/sample_form.json b/etc/sample_form.json
new file mode 100644
index 0000000..e716882
--- /dev/null
+++ b/etc/sample_form.json
@@ -0,0 +1,108 @@
+{
+   "submit" : {
+      "content" : [
+         {
+            "html" : "<h1>Account Request</h1>",
+            "type" : "raw_html"
+         },
+         {
+            "type" : "raw_html",
+            "html" : "<h2>Request Submitted</h2>"
+         },
+         {
+            "type" : "raw_html",
+            "html" : "<p>Your request has been submitted.</p>"
+         }
+      ],
+      "name" : "Submit Request",
+      "next" : ""
+   },
+   "queue" : 1,
+   "page2" : {
+      "validation" : 1,
+      "name" : "Page Two",
+      "content" : [
+         {
+            "html" : "<h1>Account Request</h1>",
+            "type" : "raw_html"
+         },
+         {
+            "comp_name" : "Field",
+            "arguments" : {
+               "name" : "Requestors",
+               "label" : "Requested by",
+               "default" : "root at localhost"
+            },
+            "type" : "component"
+         },
+         {
+            "arguments" : {
+               "name" : "Systems"
+            },
+            "type" : "component",
+            "comp_name" : "Field"
+         }
+      ],
+      "next" : "page3"
+   },
+   "formtools-start-page" : "page1",
+   "page3" : {
+      "content" : [
+         {
+            "type" : "raw_html",
+            "html" : "<h1>Account Request</h1>"
+         },
+         {
+            "type" : "raw_html",
+            "html" : "<h2>Confirm Selections</h2>"
+         },
+         {
+            "type" : "component",
+            "comp_name" : "ShowChoices"
+         },
+         {
+            "type" : "raw_html",
+            "html" : "<hr/>\n<i><b>\n<p>What else should we know as we act on your request?</p>\n<p> Do you have special time constraints?</p>\n</b></i>"
+         },
+         {
+            "comp_name" : "Field",
+            "arguments" : {
+               "name" : "Content"
+            },
+            "type" : "component"
+         },
+         {
+            "input-value" : "create_ticket",
+            "type" : "hidden",
+            "input-name" : "create_ticket"
+         }
+      ],
+      "name" : "Review Selections",
+      "validation" : 1,
+      "next" : "submit"
+   },
+   "page1" : {
+      "validation" : 1,
+      "name" : "Page One",
+      "content" : [
+         {
+            "html" : "<h1>Account Request</h1>",
+            "type" : "raw_html"
+         },
+         {
+            "type" : "raw_html",
+            "html" : "<h2>Enter your preferred username</h2>"
+         },
+         {
+            "comp_name" : "Field",
+            "type" : "component",
+            "arguments" : {
+               "default" : "preferred username",
+               "show_validation" : 1,
+               "name" : "Username"
+            }
+         }
+      ],
+      "next" : "page2"
+   }
+}

commit e9989c1119285a42bdf62d74b7e1b16641567c21
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Fri Sep 1 16:57:22 2023 -0400

    Create new page to dynamically generate forms from config

diff --git a/html/Forms/dhandler b/html/Forms/dhandler
new file mode 100644
index 0000000..eb2a0eb
--- /dev/null
+++ b/html/Forms/dhandler
@@ -0,0 +1,88 @@
+<&|/FormTools/Form, next => '/Forms/' . $form_name . '/' . $form_config->{$page}{'next'},
+    validation => $form_config->{$page}{'validation'},
+    next_for_validation => '/Forms/' . $form_name . '/' . $page,
+&>
+
+<%perl>
+# Build the current page here dyamically from config
+
+foreach my $element ( @{$form_config->{$page}{'content'}} ) {
+    if ( $element->{type} eq 'raw_html' ) {
+        $m->out( $element->{html} );
+    }
+    elsif ( $element->{type} eq 'hidden' ) {
+        $m->out('<input type="hidden" class="hidden" name="' . $element->{'input-name'}
+            . '" value="' . $element->{'input-value'} . '"  />');
+    }
+    elsif ( $element->{type} eq 'component' ) {
+        $m->comp('/FormTools/' . $element->{comp_name}, %{$element->{arguments}});
+    }
+}
+</%perl>
+
+<& /FormTools/Next &>
+</&>
+<%init>
+
+my $path = $m->dhandler_arg;
+my ($form_name, $page_name);
+
+if ( $path =~ /^([\w|\s]+)\/(\w+)$/ ) {
+    $form_name = $1;
+    $page_name = $2;
+}
+else {
+    $form_name = $path;
+}
+
+# Limit to names to letters, numbers, underscore, spaces
+unless ( $form_name =~ /^[\w|\s]+$/ ) {
+    RT->Logger->error("FormTools called with invalid form name: $form_name");
+    Abort('Invalid form name');
+}
+
+if ( $page_name ) {
+    unless ( $page_name =~ /^\w+$/ ) {
+        RT->Logger->error("FormTools called with invalid page name: $page_name");
+        Abort('Invalid page name');
+    }
+}
+
+# Load FormTools configration and look for a configured
+# form with the provided name.
+my $form_attribute = RT::Attribute->new( RT->SystemUser );
+$form_attribute->LoadByCols( Name => 'FormTools Form', Description => $form_name );
+my $form_config;
+
+if ( $form_attribute->Id ) {
+    $form_config = $form_attribute->Content;
+}
+else {
+    # We didn't find a form, so show a not found page
+    Abort('Form not found');
+}
+
+my $page;
+
+if ( $page_name ) {
+    $page = $page_name;
+}
+else {
+    $page = $form_config->{'formtools-start-page'};
+}
+
+my $queue_obj = RT::Queue->new( RT->SystemUser );
+my ($ok, $msg) = $queue_obj->Load( $form_config->{'queue'} );
+
+unless ( $ok ) {
+    RT->Logger->error('FormTools unable to load queue: ' . $form_config->{'queue'});
+    Abort('Unable to load form, invalid queue');
+}
+
+$m->notes( queue => $queue_obj );
+$m->notes( page_title => $form_config->{$page}{'name'} );
+
+</%init>
+<%args>
+$_form_tools_next => undef
+</%args>

commit 6e5109aff433e0b96aa7abcf8dba7d44d42693ef
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Wed Sep 6 08:59:42 2023 -0400

    Remove unused variable
    
    $next_comp is created, but not used anywhere after
    it is set.

diff --git a/html/FormTools/Form b/html/FormTools/Form
index 7b6ba7d..665df7b 100644
--- a/html/FormTools/Form
+++ b/html/FormTools/Form
@@ -66,7 +66,6 @@ if ($validation && $real_next) {
 
     }
     unless (@results) {
-        my $next_comp = $real_next;
         $real_next = $m->caller(1)->dir_path . '/' . $real_next
             unless $real_next =~ m'^/';
         $m->subexec("$real_next", %ARGS, %request_args);

commit 51d16014358ce63a6afa790f64a311c7e6c41e7e
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Wed Sep 6 09:01:19 2023 -0400

    Update Module::Install

diff --git a/META.yml b/META.yml
index 7ab87ec..7cbfb2b 100644
--- a/META.yml
+++ b/META.yml
@@ -8,7 +8,7 @@ configure_requires:
   ExtUtils::MakeMaker: 6.59
 distribution_type: module
 dynamic_config: 1
-generated_by: 'Module::Install version 1.19'
+generated_by: 'Module::Install version 1.21'
 license: gpl
 meta-spec:
   url: http://module-build.sourceforge.net/META-spec-v1.4.html
diff --git a/inc/Module/Install.pm b/inc/Module/Install.pm
index 7ba98c2..3dd721b 100644
--- a/inc/Module/Install.pm
+++ b/inc/Module/Install.pm
@@ -31,7 +31,7 @@ BEGIN {
 	# This is not enforced yet, but will be some time in the next few
 	# releases once we can make sure it won't clash with custom
 	# Module::Install extensions.
-	$VERSION = '1.19';
+	$VERSION = '1.21';
 
 	# Storage for the pseudo-singleton
 	$MAIN    = undef;
diff --git a/inc/Module/Install/Base.pm b/inc/Module/Install/Base.pm
index 9fa42c2..67ce900 100644
--- a/inc/Module/Install/Base.pm
+++ b/inc/Module/Install/Base.pm
@@ -4,7 +4,7 @@ package Module::Install::Base;
 use strict 'vars';
 use vars qw{$VERSION};
 BEGIN {
-	$VERSION = '1.19';
+	$VERSION = '1.21';
 }
 
 # Suspend handler for "redefined" warnings
diff --git a/inc/Module/Install/Can.pm b/inc/Module/Install/Can.pm
index d65c753..93fc4f9 100644
--- a/inc/Module/Install/Can.pm
+++ b/inc/Module/Install/Can.pm
@@ -8,7 +8,7 @@ use Module::Install::Base ();
 
 use vars qw{$VERSION @ISA $ISCORE};
 BEGIN {
-	$VERSION = '1.19';
+	$VERSION = '1.21';
 	@ISA     = 'Module::Install::Base';
 	$ISCORE  = 1;
 }
diff --git a/inc/Module/Install/Fetch.pm b/inc/Module/Install/Fetch.pm
index 3072b08..3c9390a 100644
--- a/inc/Module/Install/Fetch.pm
+++ b/inc/Module/Install/Fetch.pm
@@ -6,7 +6,7 @@ use Module::Install::Base ();
 
 use vars qw{$VERSION @ISA $ISCORE};
 BEGIN {
-	$VERSION = '1.19';
+	$VERSION = '1.21';
 	@ISA     = 'Module::Install::Base';
 	$ISCORE  = 1;
 }
diff --git a/inc/Module/Install/Include.pm b/inc/Module/Install/Include.pm
index 13fdcd0..b9b926f 100644
--- a/inc/Module/Install/Include.pm
+++ b/inc/Module/Install/Include.pm
@@ -6,7 +6,7 @@ use Module::Install::Base ();
 
 use vars qw{$VERSION @ISA $ISCORE};
 BEGIN {
-	$VERSION = '1.19';
+	$VERSION = '1.21';
 	@ISA     = 'Module::Install::Base';
 	$ISCORE  = 1;
 }
diff --git a/inc/Module/Install/Makefile.pm b/inc/Module/Install/Makefile.pm
index 13a4464..1e214a0 100644
--- a/inc/Module/Install/Makefile.pm
+++ b/inc/Module/Install/Makefile.pm
@@ -8,7 +8,7 @@ use Fcntl qw/:flock :seek/;
 
 use vars qw{$VERSION @ISA $ISCORE};
 BEGIN {
-	$VERSION = '1.19';
+	$VERSION = '1.21';
 	@ISA     = 'Module::Install::Base';
 	$ISCORE  = 1;
 }
diff --git a/inc/Module/Install/Metadata.pm b/inc/Module/Install/Metadata.pm
index 11bf971..2ae8036 100644
--- a/inc/Module/Install/Metadata.pm
+++ b/inc/Module/Install/Metadata.pm
@@ -6,7 +6,7 @@ use Module::Install::Base ();
 
 use vars qw{$VERSION @ISA $ISCORE};
 BEGIN {
-	$VERSION = '1.19';
+	$VERSION = '1.21';
 	@ISA     = 'Module::Install::Base';
 	$ISCORE  = 1;
 }
@@ -455,12 +455,8 @@ sub author_from {
 my %license_urls = (
     perl         => 'http://dev.perl.org/licenses/',
     apache       => 'http://apache.org/licenses/LICENSE-2.0',
-    apache_1_1   => 'http://apache.org/licenses/LICENSE-1.1',
     artistic     => 'http://opensource.org/licenses/artistic-license.php',
-    artistic_2   => 'http://opensource.org/licenses/artistic-license-2.0.php',
     lgpl         => 'http://opensource.org/licenses/lgpl-license.php',
-    lgpl2        => 'http://opensource.org/licenses/lgpl-2.1.php',
-    lgpl3        => 'http://opensource.org/licenses/lgpl-3.0.html',
     bsd          => 'http://opensource.org/licenses/bsd-license.php',
     gpl          => 'http://opensource.org/licenses/gpl-license.php',
     gpl2         => 'http://opensource.org/licenses/gpl-2.0.php',
@@ -471,6 +467,12 @@ my %license_urls = (
     unrestricted => undef,
     restrictive  => undef,
     unknown      => undef,
+
+    # these are not actually allowed in meta-spec v1.4 but are left here for compatibility:
+    apache_1_1   => 'http://apache.org/licenses/LICENSE-1.1',
+    artistic_2   => 'http://opensource.org/licenses/artistic-license-2.0.php',
+    lgpl2        => 'http://opensource.org/licenses/lgpl-2.1.php',
+    lgpl3        => 'http://opensource.org/licenses/lgpl-3.0.html',
 );
 
 sub license {
diff --git a/inc/Module/Install/Win32.pm b/inc/Module/Install/Win32.pm
index f7aa615..b6c1d37 100644
--- a/inc/Module/Install/Win32.pm
+++ b/inc/Module/Install/Win32.pm
@@ -6,7 +6,7 @@ use Module::Install::Base ();
 
 use vars qw{$VERSION @ISA $ISCORE};
 BEGIN {
-	$VERSION = '1.19';
+	$VERSION = '1.21';
 	@ISA     = 'Module::Install::Base';
 	$ISCORE  = 1;
 }
diff --git a/inc/Module/Install/WriteAll.pm b/inc/Module/Install/WriteAll.pm
index 2db861a..d87eb9a 100644
--- a/inc/Module/Install/WriteAll.pm
+++ b/inc/Module/Install/WriteAll.pm
@@ -6,7 +6,7 @@ use Module::Install::Base ();
 
 use vars qw{$VERSION @ISA $ISCORE};
 BEGIN {
-	$VERSION = '1.19';
+	$VERSION = '1.21';
 	@ISA     = qw{Module::Install::Base};
 	$ISCORE  = 1;
 }
diff --git a/inc/YAML/Tiny.pm b/inc/YAML/Tiny.pm
index fb157a6..db3ae5c 100644
--- a/inc/YAML/Tiny.pm
+++ b/inc/YAML/Tiny.pm
@@ -2,12 +2,12 @@
 use 5.008001; # sane UTF-8 support
 use strict;
 use warnings;
-package YAML::Tiny; # git description: v1.72-7-g8682f63
+package YAML::Tiny; # git description: v1.73-12-ge02f827
 # XXX-INGY is 5.8.1 too old/broken for utf8?
 # XXX-XDG Lancaster consensus was that it was sufficient until
 # proven otherwise
 
-our $VERSION = '1.73';
+our $VERSION = '1.74';
 
 #####################################################################
 # The YAML::Tiny API.

-----------------------------------------------------------------------


hooks/post-receive
-- 
rt-extension-formtools


More information about the Bps-public-commit mailing list