Project import generated by Copybara.

GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/templates/tracker/admin-components-page.ezt b/templates/tracker/admin-components-page.ezt
new file mode 100644
index 0000000..0c9d2d1
--- /dev/null
+++ b/templates/tracker/admin-components-page.ezt
@@ -0,0 +1,203 @@
+[define category_css]css/ph_detail.css[end]
+[include "../framework/header.ezt" "showtabs"]
+
+[if-any read_only][include "../framework/read-only-rejection.ezt"]
+[else]
+
+<form action="adminComponents.do" id="adminComponents" method="POST">
+ <input type="hidden" name="token" value="form_token]">
+
+ <h4>Issue components</h4>
+ [if-any perms.EditProject]
+   <span style="margin:0 .7em">Show:
+    <select id="rowfilter">
+     <option label="All components" value="all">
+     <option label="Active components" value="active" selected=true>
+     <option label="Top-level components" value="toplevel">
+     <option label="Components I administer" value="myadmin">
+     <option label="Components I am CC'd on" value="mycc">
+     <option label="Deprecated components" value="deprecated">
+    </select>
+   </span>
+   <span style="margin:0 .7em">Select:
+     <a id="selectall" href="#">All</a>
+     <a id="selectnone" href="#">None</a>
+   </span>
+ [end]
+
+ <div class="list-foot"></div>
+ [if-any perms.EditProject]
+   <form action="adminComponents.do" method="POST">
+     <a href="/p/[projectname]/components/create" class="buttonify primary">Create component</a>
+     <input type="hidden" name="delete_components">
+     <input type="hidden" name="token" value="[form_token]">
+     <input type="submit" class="secondary" name="deletebtn" value="Delete Component(s)" disabled>
+   </form>
+   <div id="deletebtnsfeedback" class="fielderror" style="margin-left:1em">
+     [if-any failed_perm]
+       You do not have permission to delete the components:
+       [failed_perm]<br/>
+     [end]
+     [if-any failed_subcomp]
+       Can not delete the following components because they have subcomponents:
+       [failed_subcomp]<br/>
+     [end]
+     [if-any failed_templ]
+       Can not delete the following components because they are listed in templates:
+       [failed_templ]<br/>
+     [end]
+   </div>
+ [end]
+
+ <div class="section">
+   <table cellspacing="0" cellpadding="2" border="0" class="comptable results striped vt active" id="resultstable" width="100%">
+   <tbody>
+     <tr>
+       [if-any perms.EditProject]<th></th>[end]
+       <th>ID</th>
+       <th>Name</th>
+       <th>Administrators</th>
+       <th>Auto Cc</th>
+       <th>Add Labels</th>
+       <th>Description</th>
+     </tr>
+     [if-any component_defs][else]
+       <tr>
+         <td colspan="5">
+           <div style="padding: 3em; text-align: center">
+             This project has not defined any components.
+           </div>
+         </td>
+       </tr>
+     [end]
+     [for component_defs]
+       [define detail_url]/p/[projectname]/components/detail?component=[format "url"][component_defs.path][end][end]
+       <tr data-url="[detail_url]" class="comprow [component_defs.classes]">
+         [if-any perms.EditProject]
+           <td class="cb rowwidgets">
+             <input type="checkbox" data-path="[component_defs.path]" class="checkRangeSelect">
+           </td>
+         [end]
+         <td>
+            [component_defs.component_id]
+         </td>
+         <td class="id">
+           <a style="white-space:nowrap" href="[detail_url]">[component_defs.path]</a>
+         </td>
+         <td>
+           [for component_defs.admins]
+             [include "../framework/user-link.ezt" component_defs.admins][if-index component_defs.admins last][else],[end]
+           [end]
+         </td>
+         <td>
+           [for component_defs.cc]
+             [include "../framework/user-link.ezt" component_defs.cc][if-index component_defs.cc last][else],[end]
+           [end]
+         </td>
+         <td>
+           [for component_defs.labels]
+             [component_defs.labels][if-index component_defs.labels last][else],[end]
+           [end]
+         </td>
+         <td>
+             [component_defs.docstring_short]
+         </td>
+       </tr>
+     [end]
+   </tbody>
+   </table>
+ </div>[# section]
+
+ <div class="list-foot"></div>
+ [if-any perms.EditProject]
+   <form action="adminComponents.do" method="POST">
+     <a href="/p/[projectname]/components/create" class="buttonify primary">Create component</a>
+     <input type="hidden" name="delete_components">
+     <input type="hidden" name="token" value="[form_token]">
+     <input type="submit" class="secondary" name="deletebtn" value="Delete Component(s)" disabled>
+   </form>
+ [end]
+
+</form>
+
+[end]
+
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+  if ($("selectall")) {
+    $("selectall").addEventListener("click", function() {
+        _selectAllIssues();
+        setDisabled(false);
+    });
+  }
+  if ($("selectnone")) {
+    $("selectnone").addEventListener("click", function() {
+        _selectNoneIssues();
+        setDisabled(true);
+    });
+  }
+
+  var checkboxNodes = document.getElementsByClassName("checkRangeSelect");
+  var checkboxes = Array();
+  for (var i = 0; i < checkboxNodes.length; ++i) {
+    var checkbox = checkboxNodes.item(i);
+    checkboxes.push(checkbox);
+    checkbox.addEventListener("click", function (event) {
+      _checkRangeSelect(event, event.target);
+      _highlightRow(event.target);
+      updateEnabled();
+    });
+  }
+
+  function updateEnabled() {
+    var anySelected = checkboxes.some(function(checkbox) {
+      return checkbox.checked;
+    });
+    setDisabled(!anySelected);
+   }
+
+  var deleteButtons = document.getElementsByName("deletebtn");
+  function setDisabled(disabled) {
+    for (var i = 0; i < deleteButtons.length; ++i) {
+      deleteButtons.item(i).disabled = disabled;
+    }
+  }
+
+  for (var i = 0; i < deleteButtons.length; ++i) {
+    deleteButtons.item(i).addEventListener("click", function(event) {
+      var componentsToDelete = [];
+      for (var i = 0; i< checkboxes.length; ++i) {
+        var checkbox = checkboxes[[]i];
+        if (checkbox.checked)
+          componentsToDelete.push(checkbox.getAttribute("data-path"));
+      }
+      var fields = document.getElementsByName("delete_components");
+      for (var i = 0; i< fields.length; ++i) {
+        fields.item(i).value = componentsToDelete.join();
+      }
+      if (!confirm("Are you sure you want to delete the selected components ?\nThis operation cannot be undone."))
+        event.preventDefault();
+     });
+  }
+
+  function _handleResultsClick(event) {
+    var target = event.target;
+    if (target.tagName == "A" || target.type == "checkbox" || target.className == "cb")
+      return;
+    while (target && target.tagName != "TR") target = target.parentNode;
+    _go(target.attributes[[]"data-url"].value,
+        (event.metaKey || event.ctrlKey || event.button == 1));
+  };
+  _addClickListener($("resultstable"), _handleResultsClick);
+
+
+  function _handleRowFilterChange(event) {
+    $("resultstable").classList.remove('all', 'active', 'toplevel', 'myadmin', 'mycc', 'deprecated');
+    $("resultstable").classList.add(event.target.value);
+  };
+  $("rowfilter").addEventListener("change", _handleRowFilterChange);
+});
+</script>
+
+
+[include "../framework/footer.ezt"]
diff --git a/templates/tracker/admin-labels-page.ezt b/templates/tracker/admin-labels-page.ezt
new file mode 100644
index 0000000..e8cb7ae
--- /dev/null
+++ b/templates/tracker/admin-labels-page.ezt
@@ -0,0 +1,138 @@
+[define category_css]css/ph_list.css[end]
+[include "../framework/header.ezt" "showtabs"]
+[include "../framework/js-placeholders.ezt"]
+
+[if-any read_only][include "../framework/read-only-rejection.ezt"]
+[else]
+
+<form action="adminLabels.do" id="adminLabels" method="POST">
+ <input type="hidden" name="token" value="[form_token]">
+
+ <h4>Predefined issue labels</h4>
+ <div class="section">
+  [if-any perms.EditProject]
+    <table class="vt">
+     <tr><td>
+       <textarea name="predefinedlabels" rows="12" cols="75" style="tab-size:18">[labels_text]</textarea>
+       [if-any errors.label_defs]
+         <div class="fielderror">[errors.label_defs]</div>
+       [end]
+       <div>
+         Each issue may have <b>at most one</b> label with each of these prefixes:<br>
+         <input type="text" size="75" name="excl_prefixes"
+                value="[for config.excl_prefixes][config.excl_prefixes][if-index config.excl_prefixes last][else], [end][end]">
+       </div>
+      </td>
+      <td style="padding-left:.7em">
+       <div class="tip">
+           <b>Instructions:</b><br> List one label per line in desired sort-order.<br><br>
+           Optionally, use an equals-sign to document the meaning of each label.
+       </div>
+      </td>
+     </tr>
+    </table>
+  [else]
+    <table cellspacing="0" cellpadding="2" border="0" class="results striped" width="100%">
+     <tr>
+       <th style="min-width:14em">Label</th>
+       <th width="100%">Meaning</th>
+     </tr>
+     [for config.issue_labels]
+       <tr>
+         <td style="white-space:nowrap; padding-right:2em; color:#363">[config.issue_labels.name]</td>
+         <td>[config.issue_labels.docstring]</td>
+       </tr>
+     [end]
+    </table>
+  [end]
+ </div>
+
+ [if-any perms.EditProject]
+   <input type="submit" id="savechanges" name="btn" value="Save changes" class="submit">
+ [end]
+
+ <br>
+ <br>
+
+ <h4>Custom fields</h4>
+ <div class="section">
+  <table cellspacing="0" cellpadding="2" border="0" class="results striped vt" id="resultstable" width="100%">
+  <tbody>
+    <tr>
+      <th>ID</th>
+      <th>Name</th>
+      <th>Type</th>
+      <th>Required</th>
+      <th>Multivalued</th>
+      <th>Applicable to</th>
+      <th>Description</th>
+    </tr>
+    [if-any field_defs][else]
+      <tr>
+        <td colspan="40">
+          <div style="padding: 3em; text-align: center">
+            This project has not defined any custom fields.
+          </div>
+        </td>
+      </tr>
+    [end]
+    [for field_defs]
+      [define detail_url]/p/[projectname]/fields/detail?field=[field_defs.field_name][end]
+      [is field_defs.type_name "INT_TYPE"][define pretty_type_name]Integer[end][end]
+      [is field_defs.type_name "ENUM_TYPE"][define pretty_type_name]Enum[end][end]
+      [is field_defs.type_name "USER_TYPE"][define pretty_type_name]User[end][end]
+      [is field_defs.type_name "STR_TYPE"][define pretty_type_name]String[end][end]
+      [is field_defs.type_name "DATE_TYPE"][define pretty_type_name]Date[end][end]
+      [is field_defs.type_name "URL_TYPE"][define pretty_type_name]Url[end][end]
+      [is field_defs.type_name "APPROVAL_TYPE"][define pretty_type_name]Approval[end][end]
+      <tr data-url="[detail_url]">
+        <td>
+          [field_defs.field_def.field_id]
+        </td>
+        <td class="id" style="white-space:nowrap">
+          <a href="[detail_url]">[field_defs.field_name]</a></td>
+        <td style="white-space:nowrap">
+          [pretty_type_name]
+        </td>
+        <td style="white-space:nowrap">
+          [if-any field_defs.is_required_bool]Required[else]Optional[end]
+        </td>
+        <td style="white-space:nowrap">
+          [if-any field_defs.is_multivalued_bool]Multiple[else]Single[end]
+        </td>
+        <td style="white-space:nowrap">
+          [if-any field_defs.applicable_type][field_defs.applicable_type][else]Any issue[end]
+        </td>
+        <td>
+           [field_defs.docstring_short]
+        </td>
+      </tr>
+    [end]
+  </tbody>
+  </table>
+  <div class="list-foot"></div>
+  [if-any perms.EditProject]
+    <p><a href="/p/[projectname]/fields/create" class="buttonify primary">Add field</a></p>
+  [end]
+ </div>
+
+</form>
+
+[end]
+
+
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+  function _handleResultsClick(event) {
+    var target = event.target;
+    if (target.tagName == "A")
+      return;
+    while (target && target.tagName != "TR") target = target.parentNode;
+    _go(target.attributes[[]"data-url"].value,
+        (event.metaKey || event.ctrlKey || event.button == 1));
+  };
+  _addClickListener($("resultstable"), _handleResultsClick);
+});
+</script>
+
+[include "../framework/footer.ezt"]
diff --git a/templates/tracker/admin-rules-page.ezt b/templates/tracker/admin-rules-page.ezt
new file mode 100644
index 0000000..2ffc3fa
--- /dev/null
+++ b/templates/tracker/admin-rules-page.ezt
@@ -0,0 +1,17 @@
+[define category_css]css/ph_detail.css[end]
+[include "../framework/header.ezt" "showtabs"]
+
+[if-any read_only][include "../framework/read-only-rejection.ezt"]
+[else]
+
+<form action="adminRules.do" id="adminRules" method="POST">
+ <input type="hidden" name="token" value="[form_token]">
+
+ [include "../framework/filter-rule-admin-part.ezt" "with_tracking_actions"]
+
+ <input type="submit" id="savechanges" name="btn" value="Save changes" class="submit">
+</form>
+
+[end]
+
+[include "../framework/footer.ezt"]
diff --git a/templates/tracker/admin-statuses-page.ezt b/templates/tracker/admin-statuses-page.ezt
new file mode 100644
index 0000000..2d2d936
--- /dev/null
+++ b/templates/tracker/admin-statuses-page.ezt
@@ -0,0 +1,82 @@
+[define category_css]css/ph_detail.css[end]
+[include "../framework/header.ezt" "showtabs"]
+
+[if-any read_only][include "../framework/read-only-rejection.ezt"]
+[else]
+
+<form action="adminStatuses.do" id="adminStatuses" method="POST">
+ <input type="hidden" name="token" value="[form_token]">
+
+  [if-any perms.EditProject]
+    <table class="vt">
+     <tr><td>
+       <h4>Open Issue Status Values</h4>
+       <div class="section">
+         <textarea name="predefinedopen" rows="6" cols="75"  style="tab-size:18">[open_text]</textarea>
+         [if-any errors.open_statuses]
+           <div class="fielderror">[errors.open_statuses]</div>
+         [end]
+       </div>
+       <h4>Closed Issue Status Values</h4>
+       <div class="section">
+         <textarea name="predefinedclosed" rows="6" cols="75"  style="tab-size:18">[closed_text]</textarea><br><br>
+         [if-any errors.closed_statuses]
+           <div class="fielderror">[errors.closed_statuses]</div>
+         [end]
+
+         If an issue's status is being set to one of these values, offer to merge issues:<br>
+         <input type="text" size="75" name="statuses_offer_merge"
+                value="[for config.statuses_offer_merge][config.statuses_offer_merge][if-index config.statuses_offer_merge last][else], [end][end]">
+       </div>
+      </td>
+      <td style="padding-left:.7em">
+       <div class="tip">
+           <b>Instructions:</b><br> List one status value per line in desired sort-order.<br><br>
+           Optionally, use an equals-sign to document the meaning of each status value.
+       </div>
+      </td>
+     </tr>
+    </table>
+  [else]
+    <h4>Open Issue Status Values</h4>
+    <div class="section">
+    <table cellspacing="0" cellpadding="2" border="0" class="results striped" width="100%">
+      <tr>
+        <th style="min-width:14em">Status</th>
+        <th width="100%">Meaning</th>
+      </tr>
+      [for config.open_statuses]
+        <tr>
+          <td style="white-space:nowrap; padding-right:2em;">[config.open_statuses.name]</td>
+          <td>[config.open_statuses.docstring]</td>
+        </tr>
+      [end]
+    </table>
+    </div>
+
+    <h4>Closed Issue Status Values</h4>
+    <div class="section">
+    <table cellspacing="0" cellpadding="2" border="0" class="results striped" width="100%">
+      <tr>
+        <th style="min-width:14em">Status</th>
+        <th width="100%">Meaning</th>
+      </tr>
+      [for config.closed_statuses]
+        <tr>
+          <td  style="white-space:nowrap; padding-right:2em;">[config.closed_statuses.name]</td>
+          <td>[config.closed_statuses.docstring]</td>
+        </tr>
+      [end]
+    </table>
+    </div>
+  [end]
+
+
+ [if-any perms.EditProject]
+   <input type="submit" id="savechanges" name="btn" value="Save changes" class="submit">
+ [end]
+</form>
+
+[end]
+
+[include "../framework/footer.ezt"]
diff --git a/templates/tracker/admin-templates-page.ezt b/templates/tracker/admin-templates-page.ezt
new file mode 100644
index 0000000..2ef36f3
--- /dev/null
+++ b/templates/tracker/admin-templates-page.ezt
@@ -0,0 +1,76 @@
+[define category_css]css/ph_detail.css[end]
+[include "../framework/header.ezt" "showtabs"]
+
+[if-any read_only][include "../framework/read-only-rejection.ezt"]
+[else]
+
+[if-any perms.EditProject]
+  <h4>Default templates</h4>
+  <div class="section" style="padding-top:0">
+    <form action="adminTemplates.do" id="adminTemplates" method="POST">
+      <input type="hidden" name="token" value="[form_token]">
+
+      <div style="margin: 2em 0 1em 0">
+        Default template for project members:
+        <select name="default_template_for_developers" id="default_template_for_developers">
+          [for config.templates]
+            <option value="[config.templates.name]" [is config.templates.template_id config.default_template_for_developers]selected[end]>[config.templates.name]</option>
+          [end]
+        </select>
+        <br><br>
+
+        Default template for non-members:
+        <select name="default_template_for_users" id="default_template_for_users">
+           [for config.templates]
+             [define offer_template_in_users_menu]No[end]
+             [is config.templates.template_id config.default_template_for_users][define offer_template_in_users_menu]Yes[end][end]
+             [if-any config.templates.members_only][else][define offer_template_in_users_menu]Yes[end][end]
+             [is offer_template_in_users_menu "Yes"]
+               <option value="[config.templates.name]" [is config.templates.template_id config.default_template_for_users]selected[end]>[config.templates.name]</option>
+             [end]
+           [end]
+         </select>
+       </div>
+
+       <input type="submit" id="savechanges" name="btn" value="Save changes" class="submit" style="margin-left:0">
+    </form>
+  </div>
+[end]
+
+<h4>Issue templates</h4>
+<div class="section">
+  <table cellspacing="0" cellpadding="2" border="0" class="results striped vt" id="resultstable" width="100%">
+    <tbody>
+      <tr>
+        <th>Name</th>
+      </tr>
+      [if-any config.templates][else]
+        <tr>
+          <td colspan="40">
+            <div style="padding: 3em; text-align: center">
+              This project has not defined any issue templates.
+            </div>
+          </td>
+        </tr>
+      [end]
+      [for config.templates]
+        [if-any config.templates.can_view perms.EditProject]
+          [define detail_url]/p/[projectname]/templates/detail?template=[format "url"][config.templates.name][end][end]
+            <tr data-url="detail_url">
+              <td style="white-space:nowrap" class="id">
+                <a href="[detail_url]">[config.templates.name]</a></td>
+              </td>
+            </tr>
+        [end]
+      [end]
+    </tbody>
+  </table>
+
+  [if-any perms.EditProject]
+    <p><a href="/p/[projectname]/templates/create" class="buttonify primary">Add template</a></p>
+  [end]
+</div>
+
+[end][# end if not read_only]
+
+[include "../framework/footer.ezt"]
diff --git a/templates/tracker/admin-views-page.ezt b/templates/tracker/admin-views-page.ezt
new file mode 100644
index 0000000..5ec5d15
--- /dev/null
+++ b/templates/tracker/admin-views-page.ezt
@@ -0,0 +1,70 @@
+[define category_css]css/ph_detail.css[end]
+[include "../framework/header.ezt" "showtabs"]
+
+[if-any read_only][include "../framework/read-only-rejection.ezt"]
+[else]
+
+<form action="adminViews.do" id="adminViews" method="POST">
+ <input type="hidden" name="token" value="[form_token]">
+
+ [include "../framework/artifact-list-admin-part.ezt" "with_grid"]
+
+<h4 id="queries">Saved queries</h4>
+<div class="section">
+
+ <div class="closed">
+  <div>Saved queries help project visitors easily view relevant issue lists.
+   <a class="ifClosed toggleHidden" href="#"
+      style="font-size:90%; margin-left:.5em">Learn more</a>
+  </div>
+
+  <div id="filterhelp" class="ifOpened help">
+      Project owners can set up saved queries to make it easier for team members to
+      quickly run common queries.  More importantly, project owners can use saved
+      queries to focus the team's attention on the issue lists that are most important
+      for the project's success.  The project's saved queries are shown in the middle
+      section of the search dropdown menu that is next to the issue search box.
+  </div>
+  <br>
+
+  [if-any perms.EditProject]
+    [include "../framework/saved-queries-admin-part.ezt" "project"]
+  [else]
+    <table cellspacing="0" cellpadding="2" border="0" class="results striped">
+      <tr>
+        <th align="left">Saved query name</th>
+        <th align="left">Search in</th>
+        <th align="left">Query</th>
+      </tr>
+      [for canned_queries]
+        <tr>
+          <td>[canned_queries.name]</td>
+          <td>
+            [define can][canned_queries.base_query_id][end]
+            [is can "1"]All issues[end]
+            [is can "2"]Open issues[end]
+            [is can "3"]Open and owned by me[end]
+            [is can "4"]Open and reported by me[end]
+            [is can "5"]Open and starred by me[end]
+            [is can "6"]New issues[end]
+            [is can "7"]Issues to verify[end]
+            [is can "8"]Open with comment by me[end]
+          </td>
+          <td>
+            [canned_queries.query]
+          </td>
+        </tr>
+      [end]
+    </table>
+  [end]
+ </div>
+</div>
+
+ [if-any perms.EditProject]
+   <input type="submit" id="savechanges" name="btn" value="Save changes" class="submit">
+ [end]
+</form>
+
+[end]
+
+[include "../framework/footer.ezt"]
diff --git a/templates/tracker/approval-change-notification-email.ezt b/templates/tracker/approval-change-notification-email.ezt
new file mode 100644
index 0000000..c6a81ea
--- /dev/null
+++ b/templates/tracker/approval-change-notification-email.ezt
@@ -0,0 +1,23 @@
+[if-any comment.amendments][#
+  ]Updates:
+[#][for comment.amendments]        [comment.amendments.field_name]: [format "raw"][comment.amendments.newvalue][end]
+[#][end][#
+  ][end]
+Comment #[comment.sequence] on issue [issue_local_id][#
+  ] by [comment.creator.display_name]: [format "raw"][summary][#
+][end]
+[approval_url]
+
+[if-any comment.content][#
+  ][for comment.text_runs][include "render-plain-text.ezt" comment.text_runs][end][#
+][else](No comment was entered for this change.)[#
+][end]
+[if-any comment.attachments][#
+  ]Attachments:
+[#][for comment.attachments][#
+  ]        [comment.attachments.filename]: [domain_url][comment.attachments.downloadurl]&inline=1[end]
+[end]
+
+You are receiving this message because you are listed as the TL/PM
+on this issue or you are/were listed as an approver for this
+issue's approval.
diff --git a/templates/tracker/component-create-page.ezt b/templates/tracker/component-create-page.ezt
new file mode 100644
index 0000000..b818435
--- /dev/null
+++ b/templates/tracker/component-create-page.ezt
@@ -0,0 +1,129 @@
+[define title]Add a Component[end]
+[define category_css]css/ph_detail.css[end]
+[include "../framework/header.ezt" "showtabs"]
+
+<a href="/p/[projectname]/adminComponents">&lsaquo; Back to component list</a><br><br>
+
+
+<h4>Add a component</h4>
+
+<form action="create.do" method="POST">
+<input type="hidden" name="token" value="[form_token]">
+
+<table cellspacing="8" class="rowmajor vt">
+
+  <tr>
+    <th width="1%">Parent:</th>
+    <td>
+      <select name="parent_path" id="parent_path">
+        <option value="">Top level</option>
+        [for component_defs]
+          <option value="[component_defs.path]" [if-any component_defs.selected]selected=true[end]>[component_defs.path]</option>
+        [end]
+      </select>
+    </td>
+    <td rowspan="10">
+      <div class="tip">
+        <p>Components should describe the structure of the software being
+          built so that issues can be related to the correct parts.</p>
+
+        <p>Please use labels instead for releases,
+           milestones, task forces, types of issues, etc.</p>
+
+        <p>Deprecated components won't be shown in autocomplete.</p>
+      </div>
+    </td>
+  </tr>
+
+  <tr>
+    <th width="1%">Name:</th>
+    <td>
+      <input id="leaf_name" name="leaf_name" size="30" value="[initial_leaf_name]"
+             class="acob">
+      <span id="leafnamefeedback" class="fielderror" style="margin-left:1em">
+          [if-any errors.leaf_name][errors.leaf_name][end]
+      </span>
+    </td>
+  </tr>
+
+  <tr>
+    <th>Description:</th>
+    <td>
+      <textarea name="docstring" rows="4" cols="75">[initial_docstring]</textarea>
+    </td>
+  </tr>
+
+  <tr>
+    <th>Admins:</th>
+    <td>
+        <textarea id="member_admins" name="admins" rows="3" cols="75">[for initial_admins][initial_admins], [end]</textarea>
+        <span id="memberadminsfeedback" class="fielderror" style="margin-left:1em">
+            [if-any errors.member_admins][errors.member_admins][end]
+        </span>
+    </td>
+  </tr>
+
+  <tr>
+    <th>Auto Cc:</th>
+    <td>
+        <textarea id="member_cc" name="cc" rows="3" cols="75">[for initial_cc][initial_cc], [end]</textarea>
+        <span id="memberccfeedback" class="fielderror" style="margin-left:1em">
+            [if-any errors.member_cc][errors.member_cc][end]
+        </span>
+    </td>
+  </tr>
+
+  <tr>
+    <th>Add Labels:</th>
+    <td>
+        <textarea id="labels" name="labels" rows="3" cols="75">[for initial_labels][initial_labels], [end]</textarea>
+        <span id="labelsfeedback" class="fielderror" style="margin-left:1em">
+            [if-any errors.labels][errors.labels][end]
+        </span>
+    </td>
+  </tr>
+
+  <tr>
+    <th>Deprecated:</th>
+    <td>
+        <input type="checkbox" id="deprecated" name="deprecated">
+    </td>
+  </tr>
+
+  <tr>
+    <td></td>
+    <td>
+      <input id="submit_btn" type="submit" name="submit" value="Create component">
+    </td>
+  </tr>
+
+</table>
+</form>
+
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+  document.getElementById('submit_btn').disabled = 'disabled';
+  document.getElementById('leaf_name').focus();
+
+  function checkSubmit() {
+    _checkLeafName(
+        '[projectname]',
+        document.getElementById('parent_path').value,
+        '', CS_env.token);
+  }
+  setInterval(checkSubmit, 700);
+
+  var acobElements = document.getElementsByClassName("acob");
+  for (var i = 0; i < acobElements.length; ++i) {
+     var el = acobElements[[]i];
+     el.addEventListener("focus", function(event) {
+         _acrob(null);
+         _acof(event);
+     });
+  }
+});
+</script>
+
+
+
+[include "../framework/footer.ezt"]
diff --git a/templates/tracker/component-detail-page.ezt b/templates/tracker/component-detail-page.ezt
new file mode 100644
index 0000000..01757d1
--- /dev/null
+++ b/templates/tracker/component-detail-page.ezt
@@ -0,0 +1,169 @@
+[# Use raw format because the title variable will be escaped when used.]
+[define title]Component [format "raw"][component_def.path][end][end]
+[define category_css]css/ph_detail.css[end]
+[include "../framework/header.ezt" "showtabs"]
+
+<a href="/p/[projectname]/adminComponents">&lsaquo; Back to component list</a><br><br>
+
+
+<h4>Component</h4>
+[if-any creator]
+  Created by <a href="[creator.profile_url]">[creator.display_name]</a> [created]<br/>
+[end]
+[if-any modifier]
+  Last modified by <a href="[modifier.profile_url]">[modifier.display_name]</a> [modified]<br/>
+[end]
+
+<br/>
+<form action="detail.do" method="POST">
+<input type="hidden" name="token" value="[form_token]">
+<input type="hidden" name="component" value="[component_def.path]">
+<table cellspacing="8" class="rowmajor vt">
+  <tr>
+    <th width="1%">Name:</th>
+    <td>
+      [if-any allow_edit]
+        [if-any component_def.parent_path][component_def.parent_path]&gt;[end]
+        <input id="leaf_name" name="leaf_name" value="[initial_leaf_name]" size="30" class="acob">
+        <span id="leafnamefeedback" class="fielderror" style="margin-left:1em">
+          [if-any errors.leaf_name][errors.leaf_name][end]
+        </span>
+      [else]
+        [component_def.path]
+      [end]
+    </td>
+  </tr>
+
+  <tr>
+    <th>Description:</th>
+    <td>
+      [if-any allow_edit]
+        <textarea name="docstring" rows="4" cols="75">[initial_docstring]</textarea>
+      [else]
+        [component_def.docstring]
+      [end]
+    </td>
+  </tr>
+
+  <tr>
+    <th>Admins:</th>
+    <td>
+      [if-any allow_edit]
+        <textarea id="member_admins" name="admins" rows="3" cols="75">[for initial_admins][initial_admins], [end]</textarea>
+        <span id="memberadminsfeedback" class="fielderror" style="margin-left:1em">
+          [if-any errors.member_admins][errors.member_admins][end]
+        </span>
+      [else]
+        [for component_def.admins]
+          <div>[include "../framework/user-link.ezt" component_def.admins]</div>
+        [end]
+      [end]
+    </td>
+  </tr>
+
+  <tr>
+    <th>Auto Cc:</th>
+    <td>
+      [if-any allow_edit]
+        <textarea id="member_cc" name="cc" rows="3" cols="75">[for initial_cc][initial_cc], [end]</textarea>
+        <span id="memberccfeedback" class="fielderror" style="margin-left:1em">
+          [if-any errors.member_cc][errors.member_cc][end]
+        </span>
+      [else]
+        [for component_def.cc]
+          <div>[include "../framework/user-link.ezt" component_def.cc]</div>
+        [end]
+      [end]
+    </td>
+  </tr>
+
+  <tr>
+    <th>Add Labels:</th>
+    <td>
+      [if-any allow_edit]
+        <textarea id="labels" name="labels" rows="3" cols="75">[for initial_labels][initial_labels], [end]</textarea>
+        <span id="labelsfeedback" class="fielderror" style="margin-left:1em">
+          [if-any errors.labels][errors.labels][end]
+        </span>
+      [else]
+        [for component_def.labels]
+          <div>[component_def.labels]</div>
+        [end]
+      [end]
+    </td>
+  </tr>
+
+  <tr>
+    <th>Deprecated:</th>
+    <td>
+        <input type="checkbox" id="deprecated" name="deprecated" [if-any initial_deprecated]checked="checked"[end]
+               [if-any allow_edit][else]disabled[end]>
+    </td>
+  </tr>
+
+  <tr>
+    <td></td>
+    <td>
+      [if-any allow_edit]
+        <div>
+          <span style="float:left;">
+            <input type="submit" name="submit" id="submit_btn" value="Submit changes">
+            <input type="submit" class="secondary" name="deletecomponent" value="Delete component"
+                   [if-any allow_delete][else]disabled[end]
+                   id="deletecomponent">
+          </span>
+          <span style="float:right;">
+            <a href="/p/[projectname]/components/create?component=[component_def.path]">Create new subcomponent</a>
+          </span>
+          <div style="clear:both;"></div>
+        </div>
+        [if-any allow_delete][else]
+          <br/><br/>
+          <b>Note:</b>
+          [if-any subcomponents]
+            <br/>
+            Can not delete this component because it has the following subcomponents:<br/>
+            [for subcomponents]<div style="margin-left:1em">[subcomponents.path]</div>[end]
+          [end]
+          [if-any templates]
+            <br/>
+            Can not delete this component because it is listed in the following templates:<br/>
+            [for templates]<div style="margin-left:1em">[templates.name]</div>[end]
+          [end]
+        [end]
+      [end]
+    </td>
+  </tr>
+
+</table>
+</form>
+
+[if-any allow_edit]
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+  function checkSubmit() {
+    _checkLeafName('[format "js"][projectname][end]', '[format "js"][component_def.parent_path][end]', '[format "js"][component_def.leaf_name][end]', CS_env.token);
+  }
+  setInterval(checkSubmit, 700);
+
+  if ($("deletecomponent")) {
+    $("deletecomponent").addEventListener("click", function(event) {
+        if (!confirm("Are you sure you want to delete [component_def.path]?\nThis operation cannot be undone."))
+          event.preventDefault();
+     });
+  }
+
+  var acobElements = document.getElementsByClassName("acob");
+  for (var i = 0; i < acobElements.length; ++i) {
+     var el = acobElements[[]i];
+     el.addEventListener("focus", function(event) {
+         _acrob(null);
+         _acof(event);
+     });
+  }
+});
+</script>
+[end]
+
+
+[include "../framework/footer.ezt"]
diff --git a/templates/tracker/field-create-page.ezt b/templates/tracker/field-create-page.ezt
new file mode 100644
index 0000000..9b0bc11
--- /dev/null
+++ b/templates/tracker/field-create-page.ezt
@@ -0,0 +1,402 @@
+[define title]Add a Field[end]
+[define category_css]css/ph_detail.css[end]
+[include "../framework/header.ezt" "showtabs"]
+
+<a href="/p/[projectname]/adminLabels">&lsaquo; Back to field list</a><br><br>
+
+
+<h4>Add a custom field</h4>
+
+<form action="create.do" method="POST">
+<input type="hidden" name="token" value="[form_token]">
+
+<table cellspacing="8" class="rowmajor vt">
+  <tr>
+    <th width="1%">Name:</th>
+    <td>
+      <input id="fieldname" name="name" size="30" value="[initial_field_name]" class="acob">
+      <span id="fieldnamefeedback" class="fielderror" style="margin-left:1em">
+          [if-any errors.field_name][errors.field_name][end]
+      </span>
+    </td>
+  </tr>
+
+  <tr>
+    <th>Description:</th>
+    <td>
+      <textarea name="docstring" rows="4" cols="75">[initial_field_docstring]</textarea>
+    </td>
+  </tr>
+
+  <tr>
+    <th>Type:</th>
+    <td>
+      <select id="field_type" name="field_type">
+        <option value="enum_type" [is initial_type "enum_type"]selected="selected"[end]>Enum</option>
+        <option value="int_type" [is initial_type "int_type"]selected="selected"[end]>Integer</option>
+        <option value="str_type" [is initial_type "str_type"]selected="selected"[end]>String</option>
+        <option value="user_type" [is initial_type "user_type"]selected="selected"[end]>User</option>
+        <option value="date_type" [is initial_type "date_type"]selected="selected"[end]>Date</option>
+        <option value="url_type" [is initial_type "url_type"]selected="selected"[end]>URL</option>
+        <option value="approval_type" [is initial_type "approval_type"]selected="selected"[end]>Approval</option>
+      </select>
+    </td>
+  </tr>
+
+  <tr class="js-make_phase_subfield">
+    <th>Issue Gate field:</th>
+    <td>
+      <input id="phase_input" type="checkbox" name="is_phase_field" class="acob"
+             [if-any initial_is_phase_field]checked="checked"[end]>
+      <label for="phase_input">This field can only belong to issue gates.</label>
+    </td>
+  </tr>
+
+  [if-any approval_names]
+  <tr class="js-make_approval_subfield">
+    <th>Parent Approval:</th>
+    <td>
+      <select id="parent_input" name="parent_approval_name">
+        <option value="" [is initial_parent_approval_name ""]selected[end]>Not an approval's subfield</option>
+        [for approval_names]
+          <option value="[approval_names]"
+                  [is initial_parent_approval_name approval_names]selected[end]
+                  >[approval_names]</option>
+        [end]
+      </select>
+    </td>
+  </tr>
+  [end]
+
+  [# TODO(jojwang): monorail:3241, evaluate how to use applicable/importance for approval subfields]
+  <tr id="applicable_row">
+    <th>Applicable:</th>
+    <td>When issue type is:
+      <select id="applicable_type" name="applicable_type">
+        <option value="" [is initial_applicable_type ""]selected="selected"[end]>Anything</option>
+        <option disabled="disabled">----</option>
+        [for well_known_issue_types]
+          <option value="[well_known_issue_types]" [is initial_applicable_type well_known_issue_types]selected="selected"[end]>[well_known_issue_types]</option>
+        [end]
+      </select>
+      [# TODO(jrobbins): AND with free-form applicability predicate.]
+    </td>
+  </tr>
+
+  <tr id="importance_row">
+    <th>Importance:</th>
+    <td>
+      <select id="importance" name="importance">
+        <option value="required" [is initial_importance "required"]selected[end]>Required when applicable</option>
+        <option value="normal" [is initial_importance "normal"]selected[end]>Offered when applicable</option>
+        <option value="niche" [is initial_importance "niche"]selected[end]>Under "Show all fields" when applicable</option>
+      </select>
+    </td>
+  </tr>
+
+  <tr id="multi_row">
+    <th>Multivalued:</th>
+    <td>
+      <input type="checkbox" name="is_multivalued" class="acob"
+             [if-any initial_is_multivalued]checked="checked"[end]>
+    </td>
+  </tr>
+
+  <tr id="choices_row" style="display:none">
+    <th>Choices:</th>
+    <td>
+      <textarea id="choices" name="choices" rows="10" cols="75" style="tab-size:12"
+                >[initial_choices]</textarea>
+    </td>
+  </tr>
+
+  <tr id="int_row" style="display:none">
+    <th>Validation:</th>
+    <td>
+      Min value: <input type="number" name="min_value" style="text-align:right; width: 4em">
+      Max value: <input type="number" name="max_value" style="text-align:right; width: 4em"><br>
+      <span class="fielderror" style="margin-left: 1em">
+          [if-any errors.min_value][errors.min_value][end]</span><br>
+    </td>
+  </tr>
+
+  <tr id="str_row" style="display:none">
+    <th>Validation:</th>
+    <td>
+      Regex: <input type="text" name="regex" size="30"><br>
+    </td>
+  </tr>
+
+  <tr id="user_row" style="display:none">
+    <th>Validation:</th>
+    <td>
+      <input type="checkbox" name="needs_member" id="needs_member" class="acob"
+             [if-any initial_needs_member]checked[end]>
+      <label for="needs_member">User must be a project member</label><br>
+      <span id="needs_perm_span" style="margin-left:1em">
+        Required permission:
+        <input type="text" name="needs_perm" id="needs_perm" size="20"
+               value="[initial_needs_perm]" class="acob">
+      </span><br>
+    </td>
+  </tr>
+  <tr id="user_row2" style="display:none">
+    <th>Permissions:</th>
+    <td>
+      The users named in this field is granted this permission on this issue:<br>
+      [# TODO(jrobbins): one-click way to specify View vs. EditIssue vs. any custom perm.]
+      <input type="text" name="grants_perm" id="grants_perm" class="acob"
+             size="20" value="[initial_grants_perm]" autocomplete="off">
+    </td>
+  </tr>
+  <tr id="user_row3" style="display:none">
+    <th>Notification:</th>
+    <td>
+      The users named in this field will be notified via email whenever:<br>
+      <select name="notify_on">
+        <option value="never" [is initial_notify_on "0"]selected="selected"[end]
+                >No notifications</option>
+        <option value="any_comment" [is initial_notify_on "1"]selected="selected"[end]
+                >Any change or comment is added</option>
+      </select>
+    </td>
+  </tr>
+
+  <tr id="date_row" style="display:none">
+    <th>Action:</th>
+    <td>
+      When this date arrives:
+      <select name="date_action">
+        <option value="no_action" [is initial_date_action "no_action"]selected="selected"[end]
+                >No action</option>
+        [# TODO(jrobbins): owner-only option.]
+        <option value="ping_participants" [is initial_date_action "ping_participants"]selected="selected"[end]
+                >Post a "ping" comment and notify all issue participants</option>
+      </select>
+    </td>
+  </tr>
+
+  <tr id="approval_row" style="display:none">
+    <th>Approvers:</th>
+    <td>
+      <input id="member_approvers" name="approver_names" size="75" value="[initial_approvers]"
+          autocomplete="off">
+      <span class="fielderror" style="margin-left:1em">
+        [if-any errors.approvers][errors.approvers][end]
+      </span>
+    </td>
+  </tr>
+
+  <tr id="approval_row2" style="display:none">
+    <th>Survey:</th>
+    <td>
+      Any information feature owners need to provide for the approval team should be requested here.
+      <textarea name="survey" rows="4" cols="75">[initial_survey]</textarea>
+    </td>
+  </tr>
+
+  <tr>
+    <th>Admins:</th>
+    <td>
+      <input id="member_admins" name="admin_names" size="75" value="[initial_admins]"
+             autocomplete="off" class="acob">
+      <span class="fielderror" style="margin-left:1em">
+          [if-any errors.field_admins][errors.field_admins][end]
+      </span>
+    </td>
+  </tr>
+
+  <tr id="field_restriction">
+    <th>Restriction
+      <i id="editors_tooltip" class="material-icons inline-icon" style="font-size:14px; vertical-align: text-bottom"
+        title="Project owners and field admins can always edit the values of a custom field.">
+      info_outline</i> :
+    </th>
+    <td style="display:flex; align-items:center">
+      <input id="editors_checkbox" type="checkbox" name="is_restricted_field" class="acob"
+             [if-any initial_is_restricted_field]checked="checked"[end]>
+      Restrict users that can edit values of this custom field.
+    </td>
+  </tr>
+  <tr id="editors_input" style="display:none">
+    <th>Editors:</th>
+    <td>
+      <input id="member_editors" name="editor_names" size="75" value="[initial_editors]"
+             autocomplete="off" class="acob" disabled>
+      <span class="fielderror" style="margin-left:1em">
+          [if-any errors.field_editors][errors.field_editors][end]
+      </span>
+    </td>
+  </tr>
+
+  <tr>
+    <td></td>
+    <td>
+      <input id="submit_btn" type="submit" name="submit" value="Create field">
+    </td>
+  </tr>
+
+</table>
+</form>
+
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+  var submit = document.getElementById('submit_btn');
+  submit.disabled = 'disabled';
+  var fieldname = document.getElementById('fieldname');
+  var oldName = '';
+  fieldname.focus();
+
+  var fieldNameRE = /^[[]a-z]([[]-_]?[[]a-z0-9])*$/i;
+
+  function checkFieldName() {
+    name = fieldname.value;
+    if (name != oldName) {
+      oldName = name;
+      feedback = document.getElementById('fieldnamefeedback');
+      submit.disabled = 'disabled';
+      if (name == '') {
+        feedback.textContent = 'Please choose a field name';
+      } else if (!fieldNameRE.test(name)) {
+        feedback.textContent = 'Invalid field name';
+      } else if (name.length > 30) {
+        feedback.textContent = 'Field name is too long';
+      } else {
+        _checkFieldNameOnServer('[projectname]', name, CS_env.token);
+      }
+    }
+  }
+
+  setInterval(checkFieldName, 700);
+
+  function updateForm(new_type) {
+    let choices_row = document.getElementById('choices_row');
+    choices_row.style.display = (new_type == 'enum_type') ? '' : 'none';
+
+    // Approval fields cannot be subfields of approvals.
+    let approval_subfield_display = (new_type == 'approval_type') ? 'none' : '';
+    let approval_subfield_rows = document.getElementsByClassName('js-make_approval_subfield');
+    Array.prototype.forEach.call(approval_subfield_rows, row => {
+      row.style.display = approval_subfield_display;
+    });
+
+    // Enum and Approval fields cannot be gate subfields.
+    let gate_subfield_display = (new_type == 'enum_type' || new_type == 'approval_type') ? 'none': '';
+    let phase_subfield_rows = document.getElementsByClassName('js-make_phase_subfield');
+    Array.prototype.forEach.call(phase_subfield_rows, row => {
+      row.style.display = gate_subfield_display;
+    });
+
+    // Prevent users from making a field a Gate and Approval subfield.
+    if ($('parent_input')) {
+      let phase_input = $('phase_input');
+      let parent_input = $('parent_input');
+      parent_input.addEventListener('change', () => {
+        if (parent_input.value === '') {
+          phase_input.disabled = false;
+        } else {
+          phase_input.disabled = true;
+        }
+      });
+      phase_input.addEventListener('change', () => {
+        if (phase_input.checked) {
+          parent_input.disabled = true;
+        } else {
+          parent_input.disabled = false;
+        }
+      });
+    };
+
+    let int_row = document.getElementById('int_row');
+    int_row.style.display = (new_type == 'int_type') ? '' : 'none';
+
+    let str_row = document.getElementById('str_row');
+    str_row.style.display = (new_type == 'str_type') ? '' : 'none';
+
+    let user_row_display = (new_type == 'user_type') ? '' : 'none';
+    document.getElementById('user_row').style.display = user_row_display;
+    document.getElementById('user_row2').style.display = user_row_display;
+    document.getElementById('user_row3').style.display = user_row_display;
+
+    let date_row_display = (new_type == 'date_type') ? '' : 'none';
+    document.getElementById('date_row').style.display = date_row_display;
+
+    let approval_row_display = (new_type == 'approval_type') ? '' : 'none';
+    let approval_row_hide = (new_type == 'approval_type') ? 'none' : '';
+    let new_type_is_approval = (new_type == 'approval_type');
+    document.getElementById(
+        'multi_row').style.display = approval_row_hide;
+    document.getElementById(
+        'importance_row').style.display = approval_row_hide;
+    document.getElementById(
+        'applicable_row').style.display = approval_row_hide;
+    document.getElementById(
+        'field_restriction').style.display = approval_row_hide;
+    if (new_type_is_approval) {
+      document.getElementById('editors_input').style.display = 'none';
+    } else {
+      if (document.getElementById('editors_checkbox').checked) {
+        document.getElementById('editors_input').style.display = '';
+      } else {
+        document.getElementById('editors_input').style.display = 'none';
+      }
+    }
+    document.getElementById(
+        'editors_checkbox').disabled = new_type_is_approval;
+    document.getElementById(
+        'member_editors').disabled = new_type_is_approval || !document.getElementById('editors_checkbox').checked;
+    document.getElementById('approval_row').style.display = approval_row_display;
+    document.getElementById('approval_row2').style.display = approval_row_display;
+  }
+
+  let type_select = document.getElementById('field_type');
+  updateForm(type_select.value);
+  type_select.addEventListener("change", function() {
+       updateForm(type_select.value);
+  });
+
+  let needs_perm_span = document.getElementById('needs_perm_span');
+  let needs_perm = document.getElementById('needs_perm');
+  function enableNeedsPerm(enable) {
+    needs_perm_span.style.color = enable ? 'inherit' : '#999';
+    needs_perm.disabled = enable ? '' : 'disabled';
+    if (!enable) needs_perm.value = '';
+  }
+  enableNeedsPerm(false);
+
+  //Enable editors input only when restricting the field.
+  document.getElementById('editors_checkbox').onchange = function() {
+    let member_editors = document.getElementById('member_editors');
+    let editors_input = document.getElementById('editors_input');
+    if (this.checked) {
+      editors_input.style.display = '';
+    } else {
+      editors_input.style.display = 'none';
+    }
+    member_editors.disabled = !this.checked;
+  };
+
+  let needs_member = document.getElementById("needs_member");
+  if (needs_member)
+    needs_member.addEventListener("change", function() {
+       enableNeedsPerm(needs_member.checked);
+    });
+
+  let acobElements = document.getElementsByClassName("acob");
+  for (let i = 0; i < acobElements.length; ++i) {
+     let el = acobElements[[]i];
+     el.addEventListener("focus", function(event) {
+         _acrob(null);
+         _acof(event);
+     });
+  }
+
+  $('member_approvers').addEventListener("focus", function(event) {
+    _acof(event);
+  });
+
+});
+</script>
+
+
+[include "../framework/footer.ezt"]
diff --git a/templates/tracker/field-detail-page.ezt b/templates/tracker/field-detail-page.ezt
new file mode 100644
index 0000000..75c09fa
--- /dev/null
+++ b/templates/tracker/field-detail-page.ezt
@@ -0,0 +1,421 @@
+[define title]Field [field_def.field_name][end]
+[define category_css]css/ph_detail.css[end]
+[include "../framework/header.ezt" "showtabs"]
+
+<a href="/p/[projectname]/adminLabels">&lsaquo; Back to field list</a><br><br>
+
+
+<h4>Custom field</h4>
+
+<form action="detail.do" method="POST">
+<input type="hidden" name="token" value="[form_token]">
+<input type="hidden" name="field" value="[field_def.field_name]">
+
+<table cellspacing="8" class="rowmajor vt">
+  <tr>
+    <th width="1%">Name:</th>
+    <td>
+      [if-any uneditable_name]
+        <input type="hidden" name="name" value="[field_def.field_name]">
+        [field_def.field_name]
+      [else][if-any allow_edit]
+        <input name="name" value="[field_def.field_name]" size="30" class="acob">
+      [else]
+        [field_def.field_name]
+      [end][end]
+    </td>
+  </tr>
+
+  <tr>
+    <th>Description:</th>
+    <td>
+      [if-any allow_edit]
+        <textarea name="docstring" rows="4" cols="75">[field_def.docstring]</textarea>
+      [else]
+        [field_def.docstring]
+      [end]
+    </td>
+  </tr>
+
+  <tr>
+    <th>Type:</th>
+    [# TODO(jrobbins): make field types editable someday.]
+    <td>[field_def.type_name]</td>
+  </tr>
+
+  [is field_def.type_name "APPROVAL_TYPE"]
+    <tr>
+      <th>Approvers:</th>
+      <td>
+        [if-any allow_edit]
+          <input id="member_approvers" name="approver_names" size="75" value="[initial_approvers]"
+            autocomplete="off">
+          <span class="fielderror" style="margin-left:1em">
+          [if-any errors.approvers][errors.approvers][end]
+          </span>
+        [else]
+          [for field_def.approvers]
+            <div>[include "../framework/user-link.ezt" field_def.approvers]</div>
+          [end]
+        [end]
+      </td>
+    </tr>
+    <tr>
+      <th>Survey:</th>
+      <td>
+        [if-any allow_edit]
+          <textarea name="survey" rows="4" cols="75">[field_def.survey]</textarea>
+        [else]
+          <table cellspacing="4" cellpadding="0" style="padding: 2px; border:2px solid #eee">
+            [for field_def.survey_questions]
+              <tr><td>[field_def.survey_questions]</td></tr>
+            [end]
+          </table>
+        [end]
+      </td>
+    </tr>
+
+    [if-any approval_subfields]
+      <tr>
+        <th>Subfields:</th>
+        <td>
+          [for approval_subfields]
+            <div><a href="/p/[projectname]/fields/detail?field=[approval_subfields.field_name]">
+              [approval_subfields.field_name]
+            </a></div>
+          [end]
+        </td>
+      </tr>
+    [end]
+  [else]
+
+    <tr>
+      <th>Issue Gate field:</th>
+      <td>
+        [if-any field_def.is_phase_field]Yes[else]No[end]
+      </td>
+    </tr>
+
+    [is field_def.field_name "Type"][else]
+    <tr>
+      <th>Applicable:</th>
+      <td>When issue type is:
+        [if-any allow_edit]
+         [define oddball_applicability]Yes[end]
+          <select id="applicable_type" name="applicable_type">
+            <option value=""
+              [is initial_applicable_type ""]
+                selected="selected"
+                [define oddball_applicability]No[end]
+              [end]
+            >Anything</option>
+            <option disabled="disabled">----</option>
+            [for well_known_issue_types]
+              <option value="[well_known_issue_types]"
+                [is initial_applicable_type well_known_issue_types]
+                  selected="selected"
+                  [define oddball_applicability]No[end]
+                [end]
+              >[well_known_issue_types]</option>
+            [end]
+            [# If an oddball type was used, keep it.]
+            [is oddball_applicability "Yes"]
+              <option value="[initial_applicable_type]" selected="selected"
+              >[initial_applicable_type]</option>
+            [end]
+          </select>
+        [else]
+          [initial_applicable_type]
+        [end]
+        [# TODO(jrobbins): editable applicable_predicate.]
+      </td>
+    </tr>
+    [end]
+
+    <tr>
+      <th>Importance:</th>
+      <td>
+        [if-any allow_edit]
+          <select id="importance" name="importance">
+            <option value="required" [is field_def.importance "required"]selected[end]>Required when applicable</option>
+            <option value="normal" [is field_def.importance "normal"]selected[end]>Offered when applicable</option>
+            <option value="niche" [is field_def.importance "niche"]selected[end]>Under "Show all fields" when applicable</option>
+          </select>
+        [else]
+          [is field_def.importance "required"]Required when applicable[end]
+          [is field_def.importance "normal"]Offered when applicable[end]
+          [is field_def.importance "niche"]Under "Show all fields" when applicable[end]
+        [end]
+      </td>
+    </tr>
+
+    <tr>
+      <th>Multivalued:</th>
+      <td>
+        [if-any allow_edit]
+          <input type="checkbox" name="is_multivalued" class="acob"
+                 [if-any field_def.is_multivalued_bool]checked="checked"[end]>
+        [else]
+          [if-any field_def.is_multivalued_bool]Yes[else]No[end]
+        [end]
+      </td>
+    </tr>
+  [end]
+
+  [# TODO(jrobbins): dynamically display validation info as field type is edited.]
+  [is field_def.type_name "ENUM_TYPE"]
+    <tr>
+      <th>Choices:</th>
+      <td>
+        [if-any allow_edit]
+          <textarea name="choices" rows="10" cols="75" style="tab-size:18" [if-any allow_edit][else]disabled="disabled"[end]
+          >[initial_choices]</textarea>
+        [else]
+          <table cellspacing="4" cellpadding="0" style="padding: 2px; border:2px solid #eee">
+            [for field_def.choices]
+              <tr>
+                <td>[field_def.choices.name]</td>
+                <td>[if-any field_def.choices.docstring]= [end][field_def.choices.docstring]</td>
+              </tr>
+            [end]
+          </table>
+        [end]
+      </td>
+    </tr>
+  [end]
+
+  [is field_def.type_name "INT_TYPE"]
+    <tr id="int_row">
+      <th>Validation:</th>
+      <td>
+        Min value:
+        <input type="number" name="min_value" style="text-align:right; width: 4em"
+               value="[field_def.min_value]" class="acob"
+               [if-any allow_edit][else]disabled="disabled"[end]>
+
+        Max value:
+        <input type="number" name="max_value" style="text-align:right; width: 4em"
+               value="[field_def.max_value]" class="acob"
+               [if-any allow_edit][else]disabled="disabled"[end]>
+        <span class="fielderror" style="margin-left:1em">
+          [if-any errors.min_value][errors.min_value][end]</span><br>
+      </td>
+    </tr>
+  [end]
+
+  [is field_def.type_name "STR_TYPE"]
+    <tr id="str_row">
+      <th>Validation:</th>
+      <td>
+        Regex: <input type="text" name="regex" size="30" value="[field_def.regex]" class="acob"><br>
+        <span class="fielderror" style="margin-left:1em"
+            >[if-any errors.regex][errors.regex][end]</span>
+      </td>
+    </tr>
+  [end]
+
+  [is field_def.type_name "USER_TYPE"]
+    <tr id="user_row">
+      <th>Validation:</th>
+      <td>
+        <input type="checkbox" name="needs_member" id="needs_member" class="acob"
+               [if-any allow_edit][else]disabled="disabled"[end]
+               [if-any field_def.needs_member_bool]checked="checked"[end]>
+        <label for="needs_member">User must be a project member</label><br>
+        <span id="needs_perm_span" style="margin-left:1em">Required permission:
+          <input type="text" name="needs_perm" id="needs_perm" size="20"
+                 value="[field_def.needs_perm]" autocomplete="off" class="acob"
+                 [if-any allow_edit][else]disabled="disabled"[end]></span><br>
+      </td>
+    </tr>
+    <tr id="user_row2">
+      <th>Permissions:</th>
+      <td>
+        The users named in this field is granted this permission on this issue:<br>
+        [# TODO(jrobbins): one-click way to specify View vs. EditIssue vs. any custom perm.]
+        <input type="text" name="grants_perm" id="grants_perm" class="acob"
+               size="20" value="[field_def.grants_perm]" autocomplete="off"
+               [if-any allow_edit][else]disabled[end]>
+      </td>
+    </tr>
+    <tr id="user_row3">
+      <th>Notification:</th>
+      <td>
+        The users named in this field will be notified via email whenever:<br>
+        <select name="notify_on" [if-any allow_edit][else]disabled[end]
+                class="acrob">
+          <option value="never" [is field_def.notify_on "0"]selected="selected"[end]
+                  >No notifications</option>
+          <option value="any_comment" [is field_def.notify_on "1"]selected="selected"[end]
+                  >Any change or comment is added</option>
+        </select>
+      </td>
+    </tr>
+  [end]
+
+  [is field_def.type_name "DATE_TYPE"]
+    <tr id="date_row">
+      <th>Action:</th>
+      <td>
+        [if-any allow_edit]
+          <select name="date_action">
+            <option value="no_action" [is field_def.date_action_str "no_action"]selected="selected"[end]
+                    >No action</option>
+            [# TODO(jrobbins): owner-only option.]
+            <option value="ping_participants" [is field_def.date_action_str "ping_participants"]selected="selected"[end]
+                    >Post a comment and notify all issue participants</option>
+          </select>
+        [else]
+          [is field_def.date_action_str "no_action"]No action[end]
+          [# TODO(jrobbins): owner-only option.]
+          [is field_def.date_action_str "ping_participants"]Post a comment and notify all issue participants[end]
+        [end]
+      </td>
+    </tr>
+  [end]
+
+  [if-any field_def.is_approval_subfield]
+    <tr>
+      <th>Parent Approval:</th>
+      <td>
+        <a href="/p/[projectname]/fields/detail?field=[field_def.parent_approval_name]">
+          [field_def.parent_approval_name]
+        </a>
+      </td>
+    </tr>
+  [end]
+
+  <th>Admins:</th>
+    <td>
+      [if-any allow_edit]
+        <input id="member_admins" name="admin_names" size="75" value="[initial_admins]"
+               autocomplete="off" class="acob">
+        <span class="fielderror" style="margin-left:1em">
+            [if-any errors.field_admins][errors.field_admins][end]
+        </span>
+      [else]
+        [for field_def.admins]
+          <div>[include "../framework/user-link.ezt" field_def.admins]</div>
+        [end]
+      [end]
+    </td>
+  </tr>
+
+  [is field_def.type_name "APPROVAL_TYPE"][else]
+
+  <tr id="editors_restriction">
+    <th>Restriction
+      <i id="editors_tooltip" class="material-icons inline-icon" style="font-size:14px; vertical-align: text-bottom"
+        title="Project owners and field admins can always edit the values of a custom field.">
+      info_outline</i> :
+    </th>
+    <td style="display:flex; align-items:center">
+      [if-any allow_edit]
+        <input id="editors_checkbox" type="checkbox" name="is_restricted_field" class="acob"
+               [if-any field_def.is_restricted_field]checked="checked"[end]>
+        Restrict users that can edit values of this custom field.
+      [else]
+        [if-any field_def.is_restricted_field]Yes[else]No[end]
+      [end]
+    </td>
+  </tr>
+  <tr id="editors_input"
+      [if-any field_def.is_restricted_field][else]style="display:none"[end]>
+    <th>Editors:</th>
+    <td>
+      [if-any allow_edit]
+        <input id="member_editors" name="editor_names" size="75" value="[initial_editors]"
+               autocomplete="off" class="acob"
+               [if-any field_def.is_restricted_field][else]disabled[end]>
+        <span class="fielderror" style="margin-left:1em">
+            [if-any errors.field_editors][errors.field_editors][end]
+        </span>
+      [else]
+        [for field_def.editors]
+          <div>[include "../framework/user-link.ezt" field_def.editors]</div>
+        [end]
+      [end]
+    </td>
+  </tr>
+
+  [end]
+
+  <tr>
+    <td></td>
+    <td>
+      [if-any allow_edit]
+        <input type="submit" name="submit" value="Save changes">
+        <input type="submit" class="secondary" name="deletefield" value="Delete Field"
+               id="deletefield">
+      [end]
+    </td>
+  </tr>
+
+</table>
+</form>
+
+
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+  var needs_perm_span = document.getElementById('needs_perm_span');
+  var needs_perm = document.getElementById('needs_perm');
+  var needs_member = document.getElementById('needs_member');
+  function enableNeedsPerm(enable) {
+    needs_perm_span.style.color = enable ? 'inherit' : '#999';
+    needs_perm.disabled = enable ? '' : 'disabled';
+    if (!enable) needs_perm.value = '';
+  }
+  [if-any allow_edit]
+    if (needs_perm)
+      enableNeedsPerm(needs_member.checked);
+  [end]
+
+  if ($("deletefield")) {
+    $("deletefield").addEventListener("click", function(event) {
+        var msg = ("Are you sure you want to delete [field_def.field_name]?\n" +
+                   "This operation cannot be undone. " +
+                   "[if-any approval_subfields]\nAll subfields will also be deleted.[end]" +
+                   "[is field_def.type_name "ENUM_TYPE"]\nEnum values will be retained on issues as labels.[end]");
+        if (!confirm(msg))
+          event.preventDefault();
+     });
+  }
+
+  [is field_def.type_name "APPROVAL_TYPE"][else]
+  //Enable editors input only when restricting the field.
+  document.getElementById('editors_checkbox').onchange = function() {
+    var member_editors = document.getElementById('member_editors');
+    var editors_input = document.getElementById('editors_input');
+    if (this.checked) {
+      editors_input.style.display = '';
+    } else {
+      editors_input.style.display = 'none';
+    }
+    member_editors.disabled = !this.checked;
+  };
+  [end]
+
+  var acobElements = document.getElementsByClassName("acob");
+  for (var i = 0; i < acobElements.length; ++i) {
+     var el = acobElements[[]i];
+     el.addEventListener("focus", function(event) {
+         _acrob(null);
+         _acof(event);
+     });
+  }
+
+  [is field_def.type_name "APPROVAL_TYPE"]
+  $('member_approvers').addEventListener("focus", function(event) {
+    _acof(event);
+  });
+  [end]
+
+  if ($("needs_member")) {
+    $("needs_member").addEventListener("change", function(event) {
+       enableNeedsPerm($("needs_member").checked);
+    });
+  }
+});
+</script>
+
+[include "../framework/footer.ezt"]
diff --git a/templates/tracker/field-value-multi-date.ezt b/templates/tracker/field-value-multi-date.ezt
new file mode 100644
index 0000000..2e5e657
--- /dev/null
+++ b/templates/tracker/field-value-multi-date.ezt
@@ -0,0 +1,41 @@
+[if-any fields.values]
+  [for fields.values]
+    <input type="date" name="custom_[fields.field_id][is arg2 ""][else]_[arg2][end]" value="[fields.values.val]"
+           [if-any fields.field_def.min_value]min="[fields.field_def.min_value]"[end]
+           [if-any fields.field_def.max_value]max="[fields.field_def.max_value]"[end]
+           [if-index fields.values first]
+             [is arg0 "hidden"][else]
+               [if-any arg1]required="required"[end]
+             [end]
+           [end]
+           style="text-align:right; width:12em" class="multivalued customfield"
+           aria-labelledby="[fields.field_id]_label">
+    [if-index fields.values first][else]
+      <u class="removeMultiFieldValueWidget">X</u>
+    [end]
+    [if-index fields.values last]
+      <u class="addMultiFieldValueWidget" data-field-id="[fields.field_id]" data-field-type="date"
+         data-validate-1="[fields.field_def.min_value]" data-validate-2="[fields.field_def.max_value]"
+         data-phase-name="[arg2]"
+         >Add a value</u>
+    [end]
+  [end]
+[else]
+    <input type="date" name="custom_[fields.field_id][is arg2 ""][else]_[arg2][end]" value=""
+           [if-any fields.field_def.min_value]min="[fields.field_def.min_value]"[end]
+           [if-any fields.field_def.max_value]max="[fields.field_def.max_value]"[end]
+           [is arg0 "hidden"][else]
+             [if-any arg1]required="required"[end]
+           [end]
+           style="text-align:right; width:12em" class="multivalued customfield"
+           aria-labelledby="[fields.field_id]_label">
+    <u class="addMultiFieldValueWidget" data-field-id="[fields.field_id]" data-field-type="date"
+       data-validate-1="[fields.field_def.min_value]" data-validate-2="[fields.field_def.max_value]"
+       data-phase-name="[arg2]">Add a value</u>
+[end]
+
+[for fields.derived_values]
+  <input type="date" disabled="disabled" value="[fields.derived_values.val]"
+         style="font-style:italic; text-align:right; width:12em" class="multivalued"
+         aria-labelledby="[fields.field_id]_label">
+[end]
diff --git a/templates/tracker/field-value-multi-enum.ezt b/templates/tracker/field-value-multi-enum.ezt
new file mode 100644
index 0000000..004b5ac
--- /dev/null
+++ b/templates/tracker/field-value-multi-enum.ezt
@@ -0,0 +1,66 @@
+[for fields.field_def.choices]
+  [define checked]No[end]
+  [define derived]No[end]
+  [for fields.values]
+    [is fields.values.val fields.field_def.choices.name]
+      [define checked]Yes[end]
+    [end]
+  [end]
+  [for fields.derived_values]
+    [is fields.derived_values.val fields.field_def.choices.name]
+      [define checked]Yes[end]
+      [define derived]Yes[end]
+    [end]
+  [end]
+
+  <label id="[fields.field_id]_[fields.field_def.choices.name]_label" class="enum_checkbox"
+         title="[is derived "Yes"]derived: [end][fields.field_def.choices.name][if-any fields.field_def.choices.docstring]: [fields.field_def.choices.docstring][end]"
+         [is derived "Yes"]style="font-style:italic"[end]>
+    <input type="checkbox" name="custom_[fields.field_id][is arg2 ""][else]_[arg2][end]"
+           value="[fields.field_def.choices.name]"
+           id="[arg0]_custom_[fields.field_id]_[fields.field_def.choices.idx]"
+           [is checked "Yes"]checked="checked"[end] [is derived "Yes"]disabled="disabled"[end]
+           aria-labelledby="[fields.field_id]_label [fields.field_id]_[fields.field_def.choices.name]_label">
+      [fields.field_def.choices.name]
+  </label>
+
+[end]
+
+
+[# Also include any oddball values as plain text with an _X_ icon.]
+[for fields.values]
+  [define already_shown]No[end]
+  [for fields.field_def.choices]
+    [is fields.field_def.choices.name fields.values.val]
+      [define already_shown]Yes[end]
+    [end]
+  [end]
+  [is already_shown "No"]
+    <span class="enum_checkbox"
+          title="This is not a defined choice for this field"
+          id="span_[arg0]_oddball_[fields.values.idx]">
+      <a id="[arg0]_oddball_[fields.values.idx]" class="remove_oddball x_icon"></a>[fields.values.val]
+      [# Below hidden input contains the value of the field for tracker_helpers._ParseIssueRequestFields ]
+      <input type="text" class="labelinput" id="input_[arg0]_oddball_[fields.values.idx]" size="20" name="label"
+             value="[fields.field_name]-[fields.values.val]" hidden>
+    </span>
+  [end]
+[end]
+
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+  var oddballAnchors = document.getElementsByClassName("remove_oddball");
+  for (var i = 0; i < oddballAnchors.length; ++i) {
+    var oddballAnchor = oddballAnchors[[]i];
+
+    oddballAnchor.addEventListener("click", function(event) {
+      var oddballSpan = $("span_" + this.id);
+      oddballSpan.style.display = "none";
+      var oddballInput = $("input_" + this.id);
+      oddballInput.value = "";
+      event.preventDefault();
+    });
+  }
+});
+</script>
+
diff --git a/templates/tracker/field-value-multi-int.ezt b/templates/tracker/field-value-multi-int.ezt
new file mode 100644
index 0000000..f17c2cf
--- /dev/null
+++ b/templates/tracker/field-value-multi-int.ezt
@@ -0,0 +1,41 @@
+[if-any fields.values]
+  [for fields.values]
+    <input type="number" name="custom_[fields.field_id][is arg2 ""][else]_[arg2][end]" value="[fields.values.val]"
+           [if-any fields.field_def.min_value]min="[fields.field_def.min_value]"[end]
+           [if-any fields.field_def.max_value]max="[fields.field_def.max_value]"[end]
+           [if-index fields.values first]
+             [is arg0 "hidden"][else]
+               [if-any arg1]required="required"[end]
+             [end]
+           [end]
+           style="text-align:right; width:12em" class="multivalued customfield"
+           aria-labelledby="[fields.field_id]_label">
+    [if-index fields.values first][else]
+      <u class="removeMultiFieldValueWidget">X</u>
+    [end]
+    [if-index fields.values last]
+      <u class="addMultiFieldValueWidget" data-field-id="[fields.field_id]" data-field-type="int"
+         data-validate-1="[fields.field_def.min_value]" data-validate-2="[fields.field_def.max_value]"
+         data-phase-name="[arg2]"
+         >Add a value</u>
+    [end]
+  [end]
+[else]
+    <input type="number" name="custom_[fields.field_id][is arg2 ""][else]_[arg2][end]" value=""
+           [if-any fields.field_def.min_value]min="[fields.field_def.min_value]"[end]
+           [if-any fields.field_def.max_value]max="[fields.field_def.max_value]"[end]
+           [is arg0 "hidden"][else]
+             [if-any arg1]required="required"[end]
+           [end]
+           style="text-align:right; width:12em" class="multivalued customfield"
+           aria-labelledby="[fields.field_id]_label">
+    <u class="addMultiFieldValueWidget" data-field-id="[fields.field_id]" data-field-type="int"
+       data-validate-1="[fields.field_def.min_value]" data-validate-2="[fields.field_def.max_value]"
+       data-phase-name="[arg2]">Add a value</u>
+[end]
+
+[for fields.derived_values]
+  <input type="number" disabled="disabled" value="[fields.derived_values.val]"
+         style="font-style:italic; text-align:right; width:12em" class="multivalued"
+         aria-labelledby="[fields.field_id]_label">
+[end]
diff --git a/templates/tracker/field-value-multi-str.ezt b/templates/tracker/field-value-multi-str.ezt
new file mode 100644
index 0000000..62c72d7
--- /dev/null
+++ b/templates/tracker/field-value-multi-str.ezt
@@ -0,0 +1,33 @@
+[if-any fields.values]
+  [for fields.values]
+    <input name="custom_[fields.field_id][is arg2 ""][else]_[arg2][end]" value="[fields.values.val]"
+           [# TODO(jrobbins): string validation]
+           [if-index fields.values first]
+             [is arg0 "hidden"][else]
+               [if-any arg1]required="required"[end]
+             [end]
+           [end]
+           style="width: 12em" class="multivalued customfield"
+           aria-labelledby="[fields.field_id]_label">
+    [if-index fields.values first][else]
+      <u class="removeMultiFieldValueWidget">X</u>
+    [end]
+    [if-index fields.values last]
+      <u class="addMultiFieldValueWidget" data-field-id="[fields.field_id]" data-field-type="str" data-phase-name="[arg2]">Add a value</u>
+    [end]
+  [end]
+[else]
+  <input name="custom_[fields.field_id][is arg2 ""][else]_[arg2][end]" value=""
+         [# TODO(jrobbins): string validation]
+         [is arg0 "hidden"][else]
+           [if-any arg1]required="required"[end]
+         [end]
+         style="width: 12em" class="multivalued customfield" aria-labelledby="[fields.field_id]_label">
+      <u class="addMultiFieldValueWidget"  data-field-id="[fields.field_id]" data-field-type="str" data-phase-name="[arg2]">Add a value</u>
+[end]
+
+[for fields.derived_values]
+  <input disabled="disabled" value="[fields.derived_values.val]"
+         style="font-style:italic" style="width: 12em" class="multivalued"
+         aria-labelledby="[fields.field_id]_label">
+[end]
diff --git a/templates/tracker/field-value-multi-url.ezt b/templates/tracker/field-value-multi-url.ezt
new file mode 100644
index 0000000..de8a3e1
--- /dev/null
+++ b/templates/tracker/field-value-multi-url.ezt
@@ -0,0 +1,29 @@
+[if-any fields.values]
+  [for fields.values]
+    <input type="text" name="custom_[fields.field_id][is arg2 ""][else]_[arg2][end]" value="[fields.values.val]"
+    [if-index fields.values first]
+      [is arg0 "hidden"][else]
+        [if-any arg1]required="required"[end]
+      [end]
+    [end]
+    style="width: 12em" class="multivalued customfield" aria-labelledby="[fields.field_id]_label">
+    [if-index fields.values first][else]
+      <u class="removeMultiFieldValueWidget">X</u>
+    [end]
+    [if-index fields.values last]
+      <u class="addMultiFieldValueWidget" data-field-id="[fields.field_id]" data-field-type="url" data-phase-name="[arg2]">Add a value</u>
+    [end]
+  [end]
+[else]
+  <input name="custom_[fields.field_id][is arg2 ""][else]_[arg2][end]" value=""
+         [is arg0 "hidden"][else]
+           [if-any arg1]required="required"[end]
+         [end]
+         style="width: 12em" class="multivalued customfield" aria-labelledby="[fields.field_id]_label">
+  <u class="addMultiFieldValueWidget" data-field-id="[fields.field_id]" data-field-type="url" data-phase-name="[arg2]">Add a value</u>
+[end]
+
+[for fields.derived_values]
+  <input disabled="disabled" value="[fields.derived_values.val]"
+         style="font-style:italic" style="width: 12em" class="multivalued" aria-labelledby="[fields.field_id]_label">
+[end]
\ No newline at end of file
diff --git a/templates/tracker/field-value-multi-user.ezt b/templates/tracker/field-value-multi-user.ezt
new file mode 100644
index 0000000..678cd79
--- /dev/null
+++ b/templates/tracker/field-value-multi-user.ezt
@@ -0,0 +1,32 @@
+[if-any fields.values]
+  [for fields.values]
+    <input type="text" name="custom_[fields.field_id][is arg2 ""][else]_[arg2][end]" value="[fields.values.val]"
+           [# TODO(jrobbins): include fields.min_value and fields.max_value attrs]
+           [if-index fields.values first]
+             [is arg0 "hidden"][else]
+               [if-any arg1]required="required"[end]
+             [end]
+           [end]
+           style="width:12em" class="multivalued userautocomplete customfield" autocomplete="off"
+           data-ac-type="owner" aria-labelledby="[fields.field_id]_label">
+        [if-index fields.values first][else]
+          <u class="removeMultiFieldValueWidget">X</u>
+        [end]
+        [if-index fields.values last]
+          <u class="addMultiFieldValueWidget" data-field-id="[fields.field_id]" data-field-type="user" data-phase-name="[arg2]">Add a value</u>
+        [end]
+  [end]
+[else]
+  <input type="text" name="custom_[fields.field_id][is arg2 ""][else]_[arg2][end]" value=""
+         [is arg0 "hidden"][else]
+           [if-any arg1]required="required"[end]
+         [end]
+         style="width:12em" class="multivalued userautocomplete customfield" autocomplete="off"
+         data-ac-type="owner" aria-labelledby="[fields.field_id]_label">
+    <u class="addMultiFieldValueWidget" data-field-id="[fields.field_id]" data-field-type="user" data-phase-name="[arg2]">Add a value</u>
+[end]
+
+[for fields.derived_values]
+  <input type="text" disabled="disabled" value="[fields.derived_values.val]"
+         style="width:12em" class="multivalued" aria-labelledby="[fields.field_id]_label">
+[end]
diff --git a/templates/tracker/field-value-single-date.ezt b/templates/tracker/field-value-single-date.ezt
new file mode 100644
index 0000000..d9f344a
--- /dev/null
+++ b/templates/tracker/field-value-single-date.ezt
@@ -0,0 +1,43 @@
+[# Even though this field definition says it is single-valued, the issue might have
+   multiple values if the field definition was previously multi-valued.  In such a situation
+   values other than the first value are shown read-only and must be explicitly removed
+   before the comment can be submitted. ]
+
+[# If the field has no explicit values, then show an empty form element.]
+[if-any fields.values][else]
+    <input type="date" name="custom_[fields.field_id][is arg2 ""][else]_[arg2][end]" id="[arg0]_custom_[fields.field_id]" value=""
+           [is arg0 "hidden"][else]
+             [if-any arg1] required="required"[end]
+           [end]
+           [if-any fields.field_def.min_value]min="[fields.field_def.min_value]"[end]
+           [if-any fields.field_def.max_value]max="[fields.field_def.max_value]"[end]
+           style="text-align:right; width:12em" class="multivalued customfield"
+           aria-labelledby="[fields.field_id]_label">
+[end]
+
+
+[for fields.values]
+  [if-index fields.values first]
+    <input type="date" name="custom_[fields.field_id][is arg2 ""][else]_[arg2][end]" value="[fields.values.val]"
+           [is arg0 "hidden"][else]
+             [if-any arg1]required="required"[end]
+           [end]
+           [if-any fields.field_def.min_value]min="[fields.field_def.min_value]"[end]
+           [if-any fields.field_def.max_value]max="[fields.field_def.max_value]"[end]
+           style="text-align:right; width:12em" class="multivalued customfield"
+           aria-labelledby="[fields.field_id]_label">
+  [else]
+    <span>
+      <input type="date" disabled="disabled" value="[fields.values.val]"
+             style="text-align:right; width: 12em" class="multivalued customfield"
+             aria-labelledby="[fields.field_id]_label">
+      <u class="removeMultiFieldValueWidget">X</u>
+    </span>
+  [end]
+[end]
+
+[for fields.derived_values]
+  <input type="date" disabled="disabled" value="[fields.derived_values.val]"
+         style="font-style:italic; text-align:right; width:12em" class="multivalued"
+	 aria-labelledby="[fields.field_id]_label">
+[end]
diff --git a/templates/tracker/field-value-single-enum.ezt b/templates/tracker/field-value-single-enum.ezt
new file mode 100644
index 0000000..eb575a6
--- /dev/null
+++ b/templates/tracker/field-value-single-enum.ezt
@@ -0,0 +1,81 @@
+[if-any fields.values fields.derived_values]
+
+  [# TODO(jrobbins): a better UX for undesired values would be to replace the current
+     --/value slect widget with a plain-text display of the value followed by an _X_
+     link to delete it.  There would be a hidden field with the value.  Validation would
+     fail in JS and on the server if each such _X_ had not already been clicked.]
+
+  [# There could be more than one if this field used to be multi-valued.]
+  [for fields.values]
+      <select name="custom_[fields.field_id][is arg2 ""][else]_[arg2][end]" id="[arg0]_custom_[fields.field_id]"
+              class="custom_field_value_menu" aria-labelledby="[fields.field_id]_label">
+          [define show_no_value_choice]No[end]
+          [# Non-required fields can have any value removed.]
+          [if-any fields.field_def.is_required_bool][else]
+            [define show_no_value_choice]Yes[end]
+          [end]
+          [# Formerly multi-valued fields need -- to narrow down to being singled valued.]
+          [if-index fields.values first][else]
+            [define show_no_value_choice]Yes[end]
+          [end]
+          [is show_no_value_choice "Yes"]
+            <option value="--"
+                    [is fields.values.val ""]selected="selected"[end]
+                    title="No value">--</option>
+          [end]
+
+          [define value_is_shown]No[end]
+          [for fields.field_def.choices]
+            [define show_choice]No[end]
+            [# Always show the current value]
+            [is fields.values.val fields.field_def.choices.name]
+              [define value_is_shown]Yes[end]
+              [define show_choice]Yes[end]
+            [end]
+            [# Formerly multi-valued fields extra values can ONLY be removed.]
+            [if-index fields.values first]
+              [define show_choice]Yes[end]
+            [end]
+            [is show_choice "Yes"]
+              <option value="[fields.field_def.choices.name]"
+                      [is fields.values.val fields.field_def.choices.name]selected="selected"[end]>
+                [fields.field_def.choices.name]
+		[if-any fields.field_def.choices.docstring]= [fields.field_def.choices.docstring][end]
+              </option>
+            [end]
+          [end]
+
+          [is value_is_shown "No"]
+            [# This is an oddball label, force the user to explicitly remove it.]
+              <option value="[fields.values.val]" selected="selected"
+                      title="This value is not a defined choice for this field">
+                [fields.values.val]
+              </option>
+          [end]
+      </select><br>
+  [end]
+
+  [for fields.derived_values]
+    <div title="Derived: [fields.derived_values.docstring]" class="rolloverzone">
+      <i>[fields.derived_values.val]</i>
+    </div>
+  [end]
+
+[else][# No current values, just give all choices.]
+
+   <select name="custom_[fields.field_id][is arg2 ""][else]_arg2[end]" id="[arg0]_custom_[fields.field_id]"
+           class="custom_field_value_menu" aria-labelledby="[fields.field_id]_label">
+       [if-any fields.field_def.is_required_bool]
+         <option value="" disabled="disabled" selected="selected">Select value&hellip;</option>
+       [else]
+          <option value="--" selected="selected" title="No value">--</option>
+       [end]
+       [for fields.field_def.choices]
+         <option value="[fields.field_def.choices.name]">
+           [fields.field_def.choices.name]
+           [if-any fields.field_def.choices.docstring]= [fields.field_def.choices.docstring][end]
+         </option>
+       [end]
+   </select><br>
+
+[end]
diff --git a/templates/tracker/field-value-single-int.ezt b/templates/tracker/field-value-single-int.ezt
new file mode 100644
index 0000000..944ba8b
--- /dev/null
+++ b/templates/tracker/field-value-single-int.ezt
@@ -0,0 +1,43 @@
+[# Even though this field definition says it is single-valued, the issue might have
+   multiple values if the field definition was previously multi-valued.  In such a situation
+   values other than the first value are shown read-only and must be explicitly removed
+   before the comment can be submitted. ]
+
+[# If the field has no explicit values, then show an empty form element.]
+[if-any fields.values][else]
+    <input type="number" name="custom_[fields.field_id][is arg2 ""][else]_[arg2][end]" id="[arg0]_custom_[fields.field_id]" value=""
+           [is arg0 "hidden"][else]
+             [if-any arg1] required="required"[end]
+           [end]
+           [if-any fields.field_def.min_value]min="[fields.field_def.min_value]"[end]
+           [if-any fields.field_def.max_value]max="[fields.field_def.max_value]"[end]
+           style="text-align:right; width:12em" class="multivalued customfield"
+           aria-labelledby="[fields.field_id]_label">
+[end]
+
+
+[for fields.values]
+  [if-index fields.values first]
+    <input type="number" name="custom_[fields.field_id][is arg2 ""][else]_[arg2][end]" value="[fields.values.val]"
+           [is arg0 "hidden"][else]
+             [if-any arg1]required="required"[end]
+           [end]
+           [if-any fields.field_def.min_value]min="[fields.field_def.min_value]"[end]
+           [if-any fields.field_def.max_value]max="[fields.field_def.max_value]"[end]
+           style="text-align:right; width:12em" class="multivalued customfield"
+           aria-labelledby="[fields.field_id]_label">
+  [else]
+    <span>
+      <input type="number" disabled="disabled" value="[fields.values.val]"
+             style="text-align:right; width: 12em" class="multivalued customfield"
+             aria-labelledby="[fields.field_id]_label">
+      <u class="removeMultiFieldValueWidget">X</u>
+    </span>
+  [end]
+[end]
+
+[for fields.derived_values]
+  <input type="number" disabled="disabled" value="[fields.derived_values.val]"
+         style="font-style:italic; text-align:right; width:12em" class="multivalued"
+	 aria-labelledby="[fields.field_id]_label">
+[end]
diff --git a/templates/tracker/field-value-single-str.ezt b/templates/tracker/field-value-single-str.ezt
new file mode 100644
index 0000000..60ff63c
--- /dev/null
+++ b/templates/tracker/field-value-single-str.ezt
@@ -0,0 +1,41 @@
+[# Even though this field definition says it is single-valued, the issue might have
+   multiple values if the field definition was previously multi-valued.  In such a situation
+   values other than the first value are shown read-only and must be explicitly removed
+   before the comment can be submitted. ]
+
+[# If the field has no explicit values, then show an empty form element.]
+[if-any fields.values][else]
+    <input name="custom_[fields.field_id][is arg2 ""][else]_[arg2][end]" id="[arg0]_custom_[fields.field_id]" value=""
+           [is arg0 "hidden"][else]
+             [if-any arg1]required="required"[end]
+           [end]
+           [# TODO(jrobbins): validation]
+           class="multivalued customfield" style="width: 12em"
+           aria-labelledby="[fields.field_id]_label">
+[end]
+
+
+[for fields.values]
+  [if-index fields.values first]
+    <input name="custom_[fields.field_id][is arg2 ""][else]_[arg2][end]" value="[fields.values.val]"
+           class="multivalued customfield"
+           [is arg0 "hidden"][else]
+             [if-any arg1]required="required"[end]
+           [end]
+           [# TODO(jrobbins): validation]
+           style="width: 12em"
+           aria-labelledby="[fields.field_id]_label"><br>
+  [else]
+    <span>
+      <input disabled="disabled" value="[fields.values.val]"
+             class="multivalued" style="width: 12em" aria-labelledby="[fields.field_id]_label">
+      <a href="#" class="removeMultiFieldValueWidget">X</a>
+    </span>
+  [end]
+[end]
+
+[for fields.derived_values]
+  <input disabled="disabled" value="[fields.derived_values.val]"
+         style="font-style:italic" class="multivalued" style="width: 12em"
+         aria-labelledby="[fields.field_id]_label"><br>
+[end]
diff --git a/templates/tracker/field-value-single-url.ezt b/templates/tracker/field-value-single-url.ezt
new file mode 100644
index 0000000..27bd9fe
--- /dev/null
+++ b/templates/tracker/field-value-single-url.ezt
@@ -0,0 +1,31 @@
+[# Even though this field definition says it is single-valued, the issue might have
+   multiple values if the field definition was previously multi-valued.  In such a situation
+   values other than the first value are shown read-only and must be explicitly removed
+   before the comment can be submitted. ]
+
+[# If the field has no explicit values, then show an empty form element.]
+[if-any fields.values][else]
+  <input name="custom_[fields.field_id][is arg2 ""][else]_[arg2][end]" id="[arg0]_custom_[fields.field_id]" value=""
+         class="multivalued customfield"
+         [is arg0 "hidden"][else]
+           [if-any arg1]required="required"[end]
+         [end]
+         style="width: 12em" aria-labelledby="[fields.field_id]_label">
+[end]
+
+[for fields.values]
+  [if-index fields.values first]
+    <input name="custom_[fields.field_id][is arg2 ""][else]_[arg2][end]" value="[fields.values.val]"
+           class="multivalued customfield"
+           [is arg0 "hidden"][else]
+             [if-any arg1]required="required"[end]
+           [end]
+           style="width: 12em" aria-labelledby="[fields.field_id]_label"><br>
+  [else]
+    <span>
+      <input disabled="disabled" value="[fields.values.val]"
+             class="multivalued" style="width: 12em" aria-labelledby="[fields.field_id]_label">
+      <a href="#" class="removeMultiFieldValueWidget">X</a>
+    </span>
+  [end]
+[end]
\ No newline at end of file
diff --git a/templates/tracker/field-value-single-user.ezt b/templates/tracker/field-value-single-user.ezt
new file mode 100644
index 0000000..5a09d00
--- /dev/null
+++ b/templates/tracker/field-value-single-user.ezt
@@ -0,0 +1,39 @@
+[# Even though this field definition says it is single-valued, the issue might have
+   multiple values if the field definition was previously multi-valued.  In such a situation
+   values other than the first value are shown read-only and must be explicitly removed
+   before the comment can be submitted. ]
+
+[# If the field has no explicit values, then show an empty form element.]
+[if-any fields.values][else]
+    <input type="text" name="custom_[fields.field_id][is arg2 ""][else]_[arg2][end]" id="[arg0]_custom_[fields.field_id]" value=""
+           [is arg0 "hidden"][else]
+             [if-any arg1]required="required"[end]
+           [end]
+           style="width:12em" class="multivalued userautocomplete customfield" autocomplete="off"
+           data-ac-type="owner" aria-labelledby="[fields.field_id]_label">
+[end]
+
+
+[for fields.values]
+  [if-index fields.values first]
+    <input type="text" name="custom_[fields.field_id][is arg2 ""][else]_[arg2][end]" value="[fields.values.val]"
+           [is arg0 "hidden"][else]
+             [if-any arg1]required="required"[end]
+           [end]
+           style="width:12em" class="multivalued userautocomplete customfield" autocomplete="off"
+           data-ac-type="owner" aria-labelledby="[fields.field_id]_label">
+  [else]
+    <span>
+      <input type="text" disabled="disabled" value="[fields.values.val]"
+             style="width:12em" class="multivalued userautocomplete customfield" autocomplete="off"
+             data-ac-type="owner" aria-labelledby="[fields.field_id]_label">
+      <a href="#" class="removeMultiFieldValueWidget">X</a>
+    </span>
+  [end]
+[end]
+
+[for fields.derived_values]
+  <input type="text" disabled="disabled" value="[fields.derived_values.val]"
+         style="font-style:italic; width:12em" class="multivalued"
+         data-ac-type="owner" aria-labelledby="[fields.field_id]_label">
+[end]
diff --git a/templates/tracker/field-value-widgets-js.ezt b/templates/tracker/field-value-widgets-js.ezt
new file mode 100644
index 0000000..127d85c
--- /dev/null
+++ b/templates/tracker/field-value-widgets-js.ezt
@@ -0,0 +1,35 @@
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+  var removeMFVElements = document.getElementsByClassName("removeMultiFieldValueWidget");
+  for (var i = 0; i < removeMFVElements.length; ++i) {
+     var el = removeMFVElements[[]i];
+     el.addEventListener("click", function(event) {
+         _removeMultiFieldValueWidget(event.target);
+     });
+  }
+
+  var addMFVElements = document.getElementsByClassName("addMultiFieldValueWidget");
+  for (var i = 0; i < addMFVElements.length; ++i) {
+     var el = addMFVElements[[]i];
+     el.addEventListener("click", function(event) {
+          var target = event.target;
+          var fieldID = target.getAttribute("data-field-id");
+          var fieldType = target.getAttribute("data-field-type");
+          var fieldValidate1 = target.getAttribute("data-validate-1");
+          var fieldValidate2 = target.getAttribute("data-validate-2");
+	  var fieldPhaseName = target.getAttribute("data-phase-name");
+         _addMultiFieldValueWidget(
+             event.target, fieldID, fieldType, fieldValidate1, fieldValidate2, fieldPhaseName);
+     });
+  }
+
+  var customFieldElements = document.getElementsByClassName("customfield");
+  for (var i = 0; i < customFieldElements.length; ++i) {
+     var el = customFieldElements[[]i];
+     el.addEventListener("focus", function(event) {
+         _acrob(null);
+         _acof(event);
+     });
+  }
+});
+</script>
diff --git a/templates/tracker/field-value-widgets.ezt b/templates/tracker/field-value-widgets.ezt
new file mode 100644
index 0000000..3593fa2
--- /dev/null
+++ b/templates/tracker/field-value-widgets.ezt
@@ -0,0 +1,56 @@
+[# Display widgets for editing one custom field.
+   The variable "fields" must already refer to a FieldValueView object.
+   arg0: True if the field is multi-valued.
+   arg1: Prefix for IDs
+   arg2: True if the field should be required
+   arg3: Parent phase name suffix if any.
+]
+[is fields.field_def.type_name "ENUM_TYPE"]
+  [if-any arg0]
+    [include "field-value-multi-enum.ezt" arg1 arg2 arg3]
+  [else]
+    [include "field-value-single-enum.ezt" arg1 arg2 arg3]
+  [end]
+[end]
+
+[is fields.field_def.type_name "INT_TYPE"]
+  [if-any arg0]
+    [include "field-value-multi-int.ezt" arg1 arg2 arg3]
+  [else]
+    [include "field-value-single-int.ezt" arg1 arg2 arg3]
+  [end]
+[end]
+
+[is fields.field_def.type_name "STR_TYPE"]
+  [if-any arg0]
+    [include "field-value-multi-str.ezt" arg1 arg2 arg3]
+  [else]
+    [include "field-value-single-str.ezt" arg1 arg2 arg3]
+  [end]
+[end]
+
+[is fields.field_def.type_name "USER_TYPE"]
+  [if-any arg0]
+    [include "field-value-multi-user.ezt" arg1 arg2 arg3]
+  [else]
+    [include "field-value-single-user.ezt" arg1 arg2 arg3]
+  [end]
+[end]
+
+[is fields.field_def.type_name "DATE_TYPE"]
+  [if-any arg0]
+    [include "field-value-multi-date.ezt" arg1 arg2 arg3]
+  [else]
+    [include "field-value-single-date.ezt" arg1 arg2 arg3]
+  [end]
+[end]
+
+[is fields.field_def.type_name "URL_TYPE"]
+  [if-any arg0]
+    [include "field-value-multi-url.ezt" arg1 arg2 arg3]
+  [else]
+    [include "field-value-single-url.ezt" arg1 arg2 arg3]
+  [end]
+[end]
+
+[# TODO(jrobbins): more field types. ]
diff --git a/templates/tracker/issue-advsearch-page.ezt b/templates/tracker/issue-advsearch-page.ezt
new file mode 100644
index 0000000..6fc89fb
--- /dev/null
+++ b/templates/tracker/issue-advsearch-page.ezt
@@ -0,0 +1,82 @@
+[define category_css]css/ph_detail.css[end]
+[include "../framework/header.ezt" "hidetabs"]
+
+<form action="advsearch.do" method="POST" style="margin:6px;margin-top:12px;" autocomplete="false">
+
+[# Note: No need for UI element permission checking here. ]
+
+<table cellspacing="0" cellpadding="4" border="0" class="advquery">
+   <tr class="focus"><td width="25%"><b>&nbsp;Find issues</b></td>
+    <td>Search within</td>
+    <td>
+       <select name="can" style="width:100%">
+        [include "issue-can-widget.ezt" "advsearch"]
+       </select>
+    </td>
+    <td width="25%" align="center" rowspan="3">
+     <input type="submit" name="btn" value="Search" style="font-size:120%">
+    </td>
+   </tr>
+   <tr class="focus"><td width="25%"></td>
+       <td>with <b>all</b> of the words</td><td><input type="text" size="25" name="words" value=""></td>
+   </tr>
+   <tr class="focus"><td></td>
+       <td><b>without</b> the words</td><td><input type="text" size="25" name="without" value=""></td>
+   </tr>
+   <tr><td>&nbsp;</td><td></td><td></td><td></td></tr>
+   [# TODO(jrobbins): allow commas ]
+   <tr><td><b>Restrict search to</b></td><td>Labels</td><td><input type="text" name="labels" id="labelsearch" size="25" value="" placeholder="All the labels" autocomplete="off"></td><td class="eg">e.g., FrontEnd Priority:High</td></tr>
+   <tr><td rowspan="5"><br>
+        <table cellspacing="0" cellpadding="0" border="0"><tr><td>
+        <div class="tip">
+            <b>Tip:</b> Search results can be<br>refined by clicking on
+            the<br>result table headings.<br> <a href="searchtips">More
+            Search Tips</a>
+        </div>
+        </td></tr></table>
+       </td>
+       [# TODO(jrobbins): allow commas ]
+       <td>Statuses</td><td><input type="text" name="statuses" id="statussearch" size="25" value="" placeholder="Any status" autocomplete="off"></td><td class="eg">e.g., Started</td></tr>
+   <tr><td>Components</td><td><input type="text" size="25" name="components" id="componentsearch" value="" placeholder="Any component" autocomplete="off"></td><td class="eg"></td></tr>
+   <tr><td>Reporters</td><td><input type="text" size="25" name="reporters" id="memberreportersearch" value="" placeholder="Any reporter" autocomplete="off"></td><td class="eg"></td></tr>
+   [# TODO(jrobbins): allow commas ]
+   <tr><td>Owners</td><td><input type="text" size="25" name="owners" id="ownersearch" value="" placeholder="Any owner" autocomplete="off"></td><td class="eg">e.g., user@example.com</td></tr>
+   <tr><td>Cc</td><td><input type="text" size="25" name="cc" id="memberccsearch" value="" placeholder="Any cc" autocomplete="off"></td><td class="eg"></td></tr>
+   <tr><td></td><td>Comment by</td><td><input type="text" size="25" name="commentby" id="membercommentbysearch" value="" placeholder="Any commenter"></td><td class="eg"></td></tr>
+   [# TODO(jrobbins): implement search by star counts
+   <tr><td></td><td>Starred by</td>
+       <td>
+           <select name="starcount" style="width:100%">
+            <option value="-1" selected="selected">Any number of users</option>
+            <option value="0">Exactly zero users</option>
+            <option value="1">1 or more users</option>
+            <option value="2">2 or more users</option>
+            <option value="3">3 or more users</option>
+            <option value="4">4 or more users</option>
+            <option value="5">5 or more users</option>
+           </select></td>
+       <td class="eg"></td>
+   </tr>
+   ]
+   [# TODO(jrobbins) search by dates? ]
+   <tr><td></td><td>&nbsp;</td><td></td><td class="eg"></td></tr>
+</table>
+</form>
+
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+  var _idsToAddDefaultListeners = [[]
+      "labelsearch", "statussearch", "componentsearch", "memberreportersearch",
+      "ownersearch", "memberccsearch", "membercommentbysearch"];
+  for (var i = 0; i < _idsToAddDefaultListeners.length; i++) {
+    var id = _idsToAddDefaultListeners[[]i];
+    if ($(id)) {
+      $(id).addEventListener("focus", function(event) {
+        _acof(event);
+      });
+    }
+  }
+});
+</script>
+
+[include "../framework/footer.ezt"]
diff --git a/templates/tracker/issue-attachment-text.ezt b/templates/tracker/issue-attachment-text.ezt
new file mode 100644
index 0000000..98e9cdf
--- /dev/null
+++ b/templates/tracker/issue-attachment-text.ezt
@@ -0,0 +1,43 @@
+[define category_css]css/ph_detail.css[end]
+[define page_css]css/d_sb.css[end]
+[# Use raw format because filename will be escaped when title variable is used.]
+[define title][format "raw"][filename][end] ([filesize])[end]
+[include "../framework/header.ezt" "showtabs"]
+
+<link type="text/css" rel="stylesheet"
+      href="[version_base]/static/css/prettify.css">
+
+<h3 style="margin-bottom: 0">Issue <a href="detail?id=[local_id][#TODO(jrobbins): comment number]">[local_id]</a> attachment: [filename] <small>([filesize])</small>
+</h3>
+
+
+
+<div class="fc">
+  [if-any too_large]
+    <p><em>This file is too large to display.</em></p>
+
+  [else][if-any is_binary]
+
+    <p><em>
+      This file is not plain text (only UTF-8 and Latin-1 text encodings are currently supported).
+    </em></p>
+  [else]
+
+    [include "../framework/file-content-part.ezt"]
+    [include "../framework/file-content-js.ezt"]
+
+  [end][end]
+
+</div>
+
+
+[if-any should_prettify]
+<script src="[version_base]/static/js/prettify.js" nonce="[nonce]"></script>
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+  prettyPrint();
+});
+</script>
+[end]
+
+[include "../framework/footer.ezt"]
diff --git a/templates/tracker/issue-blocking-change-notification-email.ezt b/templates/tracker/issue-blocking-change-notification-email.ezt
new file mode 100644
index 0000000..9e45c69
--- /dev/null
+++ b/templates/tracker/issue-blocking-change-notification-email.ezt
@@ -0,0 +1,7 @@
+Issue [issue.local_id]: [format "raw"][summary][end]
+[detail_url]
+
+[if-any is_blocking]This issue is now blocking issue [downstream_issue_ref].
+See [downstream_issue_url]
+[else]This issue is no longer blocking issue [downstream_issue_ref].
+See [downstream_issue_url][end]
diff --git a/templates/tracker/issue-bulk-change-notification-email.ezt b/templates/tracker/issue-bulk-change-notification-email.ezt
new file mode 100644
index 0000000..8a2d996
--- /dev/null
+++ b/templates/tracker/issue-bulk-change-notification-email.ezt
@@ -0,0 +1,16 @@
+[if-any any_link_only][else][if-any amendments]Updates:
+[amendments]
+[end]
+Comment[if-any commenter] by [commenter.display_name][end]:
+[if-any comment_text][format "raw"][comment_text][end][else](No comment was entered for this change.)[end]
+[end]
+Affected issues:
+[for issues]  Issue [issues.local_id]: [if-any issues.link_only][else][format "raw"][issues.summary][end][end]
+    [format "raw"]http://[hostport][issues.detail_relative_url][end]
+
+[end]
+--
+You received this message because you are listed in the owner
+or CC fields of these issues, or because you starred them.
+You may adjust your issue notification preferences at:
+http://[hostport]/hosting/settings
diff --git a/templates/tracker/issue-bulk-edit-page.ezt b/templates/tracker/issue-bulk-edit-page.ezt
new file mode 100644
index 0000000..d57c0ae
--- /dev/null
+++ b/templates/tracker/issue-bulk-edit-page.ezt
@@ -0,0 +1,483 @@
+[define category_css]css/ph_detail.css[end]
+[include "../framework/header.ezt" "hidetabs"]
+
+[# Note: base permission for this page is EditIssue]
+
+[if-any read_only][include "../framework/read-only-rejection.ezt"]
+[else]
+
+
+<div style="margin-top: 0; padding: 3px;" class="closed">
+ <form action="bulkedit.do" method="POST" style="margin: 0; padding: 0" enctype="multipart/form-data"
+       id="bulk_form">
+
+ <input type="hidden" name="can" value=[can] >
+ <input type="hidden" name="start" value=[start] >
+ <input type="hidden" name="num" value=[num] >
+ <input type="hidden" name="q" value="[query]">
+ <input type="hidden" id="sort" name="sort" value="[sortspec]">
+ <input type="hidden" name="groupby" value="[groupby]">
+ <input type="hidden" name="colspec" value="[colspec]">
+ <input type="hidden" name="x" value="[grid_x_attr]">
+ <input type="hidden" name="y" value="[grid_y_attr]">
+ <input type="hidden" name="mode" value="[if-any grid_mode]grid[end]">
+ <input type="hidden" name="cells" value="[grid_cell_mode]">
+
+ <input type="hidden" name="ids"
+        value="[for issues][issues.local_id][if-index issues last][else], [end][end]">
+ <input type="hidden" name="token" value="[form_token]">
+ <table cellpadding="0" cellspacing="0" border="0">
+  <tr><td>
+
+ <table cellspacing="0" cellpadding="3" border="0" class="rowmajor vt">
+   <tr><th>Issues:</th>
+    <td colspan="2">
+     [for issues]
+      <a href="detail?id=[issues.local_id]" title="[issues.summary]"
+        [if-any issues.closed]class=closed_ref[end]
+        >[if-any issues.closed]&nbsp;[end][issues.local_id][if-any issues.closed]&nbsp;[end]</a>[if-index issues last][else], [end]
+     [end]
+    </td>
+   </tr>
+
+   <tr>
+    <th>Comment:</th>
+    <td colspan="2">
+     <textarea cols="75" rows="6" name="comment" id="comment" class="issue_text">[initial_comment]</textarea>
+       [if-any errors.comment]
+         <div class="fielderror">[errors.comment]</div>
+       [end]
+    </td>
+   </tr>
+
+   <tr><th width="10%"><label for="statusenter">Status:</label></th><td colspan="2">
+        <select id="statusenter" name="status">
+          <option style="display: none" value="[initial_status]"></option>
+        </select>
+        <span id="merge_area" style="margin-left:2em;">
+               Merge into issue:
+               <input type="text" id="merge_into" name="merge_into" style="width: 5em"
+                      value="[is initial_merge_into "0"][else][initial_merge_into][end]">
+        </span>
+        [if-any errors.merge_into_id]
+          <div class="fielderror">[errors.merge_into_id]</div>
+        [end]
+       </td>
+   </tr>
+   <tr><th width="10%">Owner:</th><td colspan="2">
+         [include "issue-bulk-operator-part.ezt" "ownerenter" ""]
+         <input id="ownerenter" type="text" autocomplete="off" style="width: 12em"
+                name="owner" value="[initial_owner]">
+         [if-any errors.owner]
+           <div class="fielderror">[errors.owner]</div>
+         [end]
+       </td>
+   </tr>
+   <tr><th>Cc:</th><td colspan="2">
+         [include "issue-bulk-operator-part.ezt" "memberenter" "multi"]
+         <input type="text" multiple id="memberenter" autocomplete="off" style="width: 30em"
+                name="cc" value="[initial_cc]">
+         [if-any errors.cc]
+           <div class="fielderror">[errors.cc]</div>
+         [end]
+       </td>
+   </tr>
+
+   <tr><th>Components:</th><td colspan="2">
+       [include "issue-bulk-operator-part.ezt" "componententer" "multi"]
+       <input type="text" id="componententer" style="width:30em"
+              name="components" value="[initial_components]">
+       [if-any errors.components]
+         <div class="fielderror">[errors.components]</div>
+       [end]
+   </td></tr>
+
+   <tbody class="collapse">
+   [# Show some field editing elements immediately, others can be revealed.]
+     [define any_fields_to_reveal]No[end]
+     [for fields]
+       [if-any fields.applicable][if-any fields.is_editable]
+         [# TODO(jrobbins): determine applicability dynamically and update fields in JS]
+         <tr [if-any fields.display][else]class="ifExpand"[define any_fields_to_reveal]Yes[end][end]>
+           <th>[fields.field_name]:</th>
+           <td colspan="2">
+             [define widget_id]custom_[fields.field_id][end]
+             [define multi][if-any fields.field_def.is_multivalued_bool]multi[end][end]
+             [include "issue-bulk-operator-part.ezt" widget_id multi]
+             [include "field-value-widgets.ezt" False "" fields.field_def.is_required_bool ""]
+             <div class="fielderror" style="display:none" id="error_custom_[fields.field_id]"></div>
+           </td>
+         <tr>
+       [end][end]
+     [end]
+     [is any_fields_to_reveal "Yes"]
+       <tr class="ifCollapse">
+         <td colspan="2"><a href="#" class="toggleCollapse">Show all fields</a><t/td>
+       </tr>
+     [end]
+   </tbody>
+
+   [for issue_phase_names]
+     [for fields]
+       [is fields.phase_name issue_phase_names][if-any fields.is_editable]
+         [# TODO(jojwang): monorail:5154, bulk-editing single phase values not supported]
+         [if-any fields.field_def.is_multivalued_bool]
+           <tr><th>[issue_phase_names].[fields.field_name]:</th>
+             <td colspan="2">
+               [define widget_id]custom_[fields.field_id]_[issue_phase_names][end]
+               [include "issue-bulk-operator-part.ezt" widget_id "multi"]
+               [include "field-value-widgets.ezt" False "" fields.field_def.is_required_bool issue_phase_names]
+               <div class="fielderror" style="display:none" id="error_custom_[issue_phase_names]_[fields.field_id]"></div>
+             </td>
+           </tr>
+         [end]
+       [end][end]
+     [end]
+   [end]
+
+   <tr><th>Labels:</th>
+       <td colspan="2" class="labelediting">
+        <div id="enterrow1">
+         <input type="text" class="labelinput" id="label0" size="20" autocomplete="off"
+                name="label" value="[label0]">
+         <input type="text" class="labelinput" id="label1" size="20" autocomplete="off"
+                name="label" value="[label1]">
+         <input type="text" class="labelinput" id="label2" size="20" autocomplete="off"
+                data-show-id="enterrow2" data-hide-id="addrow1"
+                name="label" value="[label2]"> <span id="addrow1" class="fakelink" data-instead="enterrow2">Add a row</span>
+        </div>
+        <div id="enterrow2"  style="display:none">
+         <input type="text" class="labelinput" id="label3" size="20" autocomplete="off"
+                name="label" value="[label3]">
+         <input type="text" class="labelinput" id="label4" size="20" autocomplete="off"
+                name="label" value="[label4]">
+         <input type="text" class="labelinput" id="label5" size="20" autocomplete="off"
+                data-show-id="enterrow3" data-hide-id="addrow2"
+                name="label" value="[label5]"> <span id="addrow2" class="fakelink" data-instead="enterrow3">Add a row</span>
+        </div>
+        <div id="enterrow3" style="display:none">
+         <input type="text" class="labelinput" id="label6" size="20" autocomplete="off"
+                name="label" value="[label6]">
+         <input type="text" class="labelinput" id="label7" size="20" autocomplete="off"
+                name="label" value="[label7]">
+         <input type="text" class="labelinput" id="label8" size="20" autocomplete="off"
+                data-show-id="enterrow4" data-hide-id="addrow3"
+                name="label" value="[label8]"> <span id="addrow3" class="fakelink" data-instead="enterrow4">Add a row</span>
+        </div>
+        <div id="enterrow4" style="display:none">
+         <input type="text" class="labelinput" id="label9" size="20" autocomplete="off"
+                name="label" value="[label9]">
+         <input type="text" class="labelinput" id="label10" size="20" autocomplete="off"
+                name="label" value="[label10]">
+         <input type="text" class="labelinput" id="label11" size="20" autocomplete="off"
+                data-show-id="enterrow5" data-hide-id="addrow4"
+                name="label" value="[label11]"> <span id="addrow4" class="fakelink" data-instead="enterrow5">Add a row</span>
+        </div>
+        <div id="enterrow5" style="display:none">
+         <input type="text" class="labelinput" id="label12" size="20" autocomplete="off"
+                name="label" value="[label12]">
+         <input type="text" class="labelinput" id="label13" size="20" autocomplete="off"
+                name="label" value="[label13]">
+         <input type="text" class="labelinput" id="label14" size="20" autocomplete="off"
+                data-show-id="enterrow6" data-hide-id="addrow5"
+                name="label" value="[label14]"> <span id="addrow5" class="fakelink" data-instead="enterrow6">Add a row</span>
+        </div>
+        <div id="enterrow6" style="display:none">
+         <input type="text" class="labelinput" id="label15" size="20" autocomplete="off"
+                name="label" value="[label15]">
+         <input type="text" class="labelinput" id="label16" size="20" autocomplete="off"
+                name="label" value="[label16]">
+         <input type="text" class="labelinput" id="label17" size="20" autocomplete="off"
+                data-show-id="enterrow7" data-hide-id="addrow6"
+                name="label" value="[label17]"> <span id="addrow6" class="fakelink" data-instead="enterrow7">Add a row</span>
+        </div>
+        <div id="enterrow7" style="display:none">
+         <input type="text" class="labelinput" id="label18" size="20" autocomplete="off"
+                name="label" value="[label18]">
+         <input type="text" class="labelinput" id="label19" size="20" autocomplete="off"
+                name="label" value="[label19]">
+         <input type="text" class="labelinput" id="label20" size="20" autocomplete="off"
+                data-show-id="enterrow8" data-hide-id="addrow7"
+                name="label" value="[label20]"> <span id="addrow7" class="fakelink" data-instead="enterrow8">Add a row</span>
+        </div>
+        <div id="enterrow8" style="display:none">
+         <input type="text" class="labelinput" id="label21" size="20" autocomplete="off"
+                name="label" value="[label21]">
+         <input type="text" class="labelinput" id="label22" size="20" autocomplete="off"
+                name="label" value="[label22]">
+         <input type="text" class="labelinput" id="label23" size="20" autocomplete="off"
+                name="label" value="[label23]">
+        </div>
+      </td>
+   </tr>
+
+   <tr><th>Blocked on:</th><td colspan="2">
+         [include "issue-bulk-operator-part.ezt" "blockedonenter" "multi"]
+         <input type="text" multiple id="blockedonenter" style="width: 30em"
+                name="blocked_on" value="[initial_blocked_on]">
+         [if-any errors.blocked_on]
+           <div class="fielderror">[errors.blocked_on]</div>
+         [end]
+       </td>
+   </tr>
+
+   <tr><th>Blocking:</th><td colspan="2">
+         [include "issue-bulk-operator-part.ezt" "blockingenter" "multi"]
+         <input type="text" multiple id="blockingenter" style="width: 30em"
+                name="blocking" value="[initial_blocking]">
+         [if-any errors.blocking]
+           <div class="fielderror">[errors.blocking]</div>
+         [end]
+       </td>
+   </tr>
+
+   [if-any page_perms.DeleteIssue]
+   <tr><th width="10%">Move to project:</th><td colspan="2">
+         <input id="move_toenter" type="text" autocomplete="off" style="width: 12em"
+                name="move_to">
+         [if-any errors.move_to]
+           <div class="fielderror">[errors.move_to]</div>
+         [end]
+       </td>
+   </tr>
+   [end]
+
+   <tr>
+    <td colspan="3"><span id="confirmarea" class="novel" style="padding-top:5px; margin:0">
+      <span id="confirmmsg"></span>
+      [# TODO(jrobbins): <a href="TODO" target="_new">Learn more</a>]
+    </span>
+    </td>
+   </tr>
+ </table>
+
+
+
+[# TODO(jrobbins):     <a class="ifClosed toggleHidden" href="#">More options</a>]
+[#     <a class="ifOpened" href="#" class="toggleHidden" style="background:#ccc; padding: 4px;">Hide options</a>]
+[#     <div  class="ifOpened"  style="background:#ccc; padding: 8px"><a href="#autmatically-generated">Bookmarkable link to these values</a></div>]
+[# <br><br>]
+
+
+
+
+ <div style="padding:6px">
+  <input type="submit" id="submit_btn" name="btn" value="Update [num_issues] Issue[is num_issues "1"][else]s[end]">
+  <input type="button" id="discard" name="nobtn" value="Discard">
+
+  <input type="checkbox" checked="checked" name="send_email" id="send_email" style="margin-left:1em">
+  <label for="send_email" title="Send issue change notifications to interested users">Send email</label>
+
+ </div>
+
+
+
+[if-any show_progress]
+ <div>Note: Updating [num_issues] issues will take approximately [num_seconds] seconds.</div>
+ <div id="progress">
+ </div>
+[end]
+
+   </td>
+   <td>
+     <div class="tip">
+         <b>Usage:</b> This form allows you to update several issues at one
+         time.<br><br>
+         The same comment will be applied to all issues.<br><br>
+
+         If specified, the status or owner you enter will be applied to all
+         issues.<br><br>
+
+         You may append or remove values in multi-valued fields by choosing the += or -= operators.
+         To remove labels, preceed the label with a leading dash.  (You may also use a leading dash
+         to remove individual items when using the += operator.)
+     </div>
+   </td>
+   </tr>
+   </table>
+
+
+ </form>
+</div>
+
+<mr-bulk-approval-update
+  projectName="[projectname]"
+  localIdsStr="[local_ids_str]"
+></mr-bulk-approval-update>
+
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+  document.getElementById('comment').select();
+  _lfidprefix = 'label';
+  setTimeout(_forceProperTableWidth, 100);
+
+  _exposeExistingLabelFields();
+
+  [if-any errors.custom_fields]
+    var field_error;
+    [for errors.custom_fields]
+      field_error = document.getElementById('error_custom_' + [errors.custom_fields.field_id]);
+      field_error.textContent = "[errors.custom_fields.message]";
+      field_error.style.display = "";
+    [end]
+  [end]
+
+  checksubmit();
+  setInterval(checksubmit, 700); [# catch changes that were not keystrokes, e.g., paste menu item.]
+
+
+
+function checksubmit() {
+  var submit = document.getElementById('submit_btn');
+  var cg = document.getElementById('cg');
+  if (cg != undefined) { submit.disabled='disabled'; }
+
+  submit.disabled='disabled';
+  var restrict_to_known = [if-any restrict_to_known]true[else]false[end];
+  var confirmmsg = document.getElementById('confirmmsg');
+  var statusenter = $('statusenter');
+  var merge_area = $('merge_area');
+  var statuses_offer_merge = [[] [for statuses_offer_merge]"[statuses_offer_merge]"[if-index statuses_offer_merge last][else],[end][end] ];
+  if (restrict_to_known && confirmmsg && confirmmsg.textContent.length > 0) {
+    return;
+  }
+  if (cg == undefined || cg.value.length > 1) {
+    submit.disabled='';
+  }
+
+  if (statusenter) {
+     var offer_merge = 'none';
+     for (var i = 0; i < statuses_offer_merge.length; i++) {
+       if (statusenter.value == statuses_offer_merge[[]i]) offer_merge = '';
+     }
+     merge_area.style.display = offer_merge;
+  }
+}
+
+
+function disableFormElement(el) {
+  el.readOnly = 'yes';
+  el.style.background = '#eee';
+  [# TODO(jrobbins): disable auto-complete ]
+}
+
+
+function bulkOnSubmit() {
+  var inputs = document.getElementsByTagName('input');
+  for (var i = 0; i < inputs.length; i++) {
+    disableFormElement(inputs[[]i]);
+  }
+  disableFormElement(document.getElementById('comment'));
+  [if-any show_progress]
+   var progress = document.getElementById('progress');
+   progress.textContent = 'Processing...';
+  [end]
+}
+
+
+function _checkAutoClear(inputEl, selectID) {
+  var val = inputEl.value;
+  var sel = document.getElementById(selectID);
+  if (val.match(/^--+$/)) {
+    sel.value = 'clear';
+    inputEl.value = '';
+  } else if (val) {
+    sel.value = 'set';
+  }
+}
+
+
+$("bulk_form").addEventListener("submit", bulkOnSubmit);
+
+if ($("statusenter")) {
+  _loadStatusSelect("[projectname]", "statusenter", "[initial_status]", isBulkEdit=true);
+  $("statusenter").addEventListener("focus", function(event) {
+    _acrob(null);
+  });
+  $("statusenter").addEventListener("keyup", function(event) {
+    _checkAutoClear(event.target, "op_statusenter");
+    return _confirmNovelStatus(event.target);
+  });
+}
+if ($("ownerenter")) {
+  $("ownerenter").addEventListener("focus", function(event) {
+    _acof(event);
+  });
+  $("ownerenter").addEventListener("keyup", function(event) {
+    _checkAutoClear(event.target, "op_ownerenter");
+    return true;
+  });
+}
+if ($("memberenter")) {
+  $("memberenter").addEventListener("focus", function(event) {
+    _acof(event);
+  });
+}
+if ($("componententer")) {
+  $("componententer").addEventListener("focus", function(event) {
+    _acof(event);
+  });
+}
+
+if ($("move_toenter")) {
+  $("move_toenter").addEventListener("focus", function(event) {
+    _acof(event);
+  });
+}
+
+if ($("submit_btn")) {
+  $("submit_btn").addEventListener("focus", function(event) {
+    _acrob(null);
+  });
+  $("submit_btn").addEventListener("mousedown", function(event) {
+    _acrob(null);
+  });
+  $("submit_btn").addEventListener("click", function(event) {
+    _trimCommas();
+  });
+}
+if ($("discard")) {
+  $("discard").addEventListener("click", function(event) {
+    _confirmDiscardEntry(this);
+    event.preventDefault();
+  });
+}
+
+var labelInputs = document.getElementsByClassName("labelinput");
+for (var i = 0; i < labelInputs.length; ++i) {
+  var labelInput = labelInputs[[]i];
+  labelInput.addEventListener("keyup", function (event) {
+    if (event.target.getAttribute("data-show-id") &&
+        event.target.getAttribute("data-hide-id") &&
+        event.target.value) {
+      _showID(event.target.getAttribute("data-show-id"));
+      _hideID(event.target.getAttribute("data-hide-id"));
+    }
+    return _vallab(event.target);
+  });
+  labelInput.addEventListener("blur", function (event) {
+    return _vallab(event.target);
+  });
+  labelInput.addEventListener("focus", function (event) {
+    return _acof(event);
+  });
+}
+
+var addRowLinks = document.getElementsByClassName("fakelink");
+for (var i = 0; i < addRowLinks.length; ++i) {
+  var rowLink = addRowLinks[[]i];
+  rowLink.addEventListener("click", function (event) {
+      _acrob(null);
+      var insteadID = event.target.getAttribute("data-instead");
+      if (insteadID)
+        _showInstead(insteadID, this);
+  });
+}
+
+});
+</script>
+
+[end]
+
+[include "field-value-widgets-js.ezt"]
+[include "../framework/footer.ezt"]
diff --git a/templates/tracker/issue-bulk-operator-part.ezt b/templates/tracker/issue-bulk-operator-part.ezt
new file mode 100644
index 0000000..8b1f37a
--- /dev/null
+++ b/templates/tracker/issue-bulk-operator-part.ezt
@@ -0,0 +1,29 @@
+[# Display a <select> widget with options to set/append/remove/clear the field.
+   Args:
+    arg0: element ID of widget to disable if Clear is selected.  The form name and ID
+          of the <select> will be "op_" + arg0.
+    arg1: "multi" for multi-valued fields so that "Append" and "Remove" are offered.
+  ]
+<select name="op_[arg0]" id="op_[arg0]" style="width:9em" tabindex="-1">
+  [is arg1 "multi"]
+    <option value="append" selected="selected">Append +=</option>
+    <option value="remove">Remove -=</option>
+    [# TODO(jrobbins): <option value="setexact">Set exactly :=</option>]
+  [else]
+    <option value="set" selected="selected">Set =</option>
+    <option value="clear">Clear</option>
+  [end]
+</select>
+
+[is arg1 "multi"][else]
+<script type="text/javascript" nonce="[nonce]">
+
+runOnLoad(function() {
+  if ($("op_[arg0]")) {
+    $("op_[arg0]").addEventListener("change", function(event) {
+      TKR_ignoreWidgetIfOpIsClear(event.target, '[arg0]');
+    });
+  }
+});
+</script>
+[end]
diff --git a/templates/tracker/issue-can-widget.ezt b/templates/tracker/issue-can-widget.ezt
new file mode 100644
index 0000000..b0f8958
--- /dev/null
+++ b/templates/tracker/issue-can-widget.ezt
@@ -0,0 +1,82 @@
+[# This is used in the issue search form and issue advanced search page.  We want to show the same options in both contexts.]
+[define selected]False[end]
+<option disabled="disabled">Search within:</option>
+<option value="1" [is can "1"]selected=selected [define selected]True[end] [end]
+        title="All issues in the project">&nbsp;All issues</option>
+<option value="2" [is can "2"]selected=selected [define selected]True[end] [end]
+        title="All issues except ones with a closed status">&nbsp;Open issues</option>
+
+[if-any logged_in_user]
+ [define username][logged_in_user.email][end]
+ [is arg0 "admin"][define username]logged-in-user[end][end]
+ <option value="3" [is can "3"]selected=selected [define selected]True[end] [end]
+         title="[[]Open issues] owner=[username]">&nbsp;Open and owned by me</option>
+ <option value="4" [is can "4"]selected=selected [define selected]True[end] [end]
+         title="[[]Open issues] reporter=[username]">&nbsp;Open and reported by me</option>
+ <option value="5" [is can "5"]selected=selected [define selected]True[end] [end]
+         title="[[]Open issues] starredby:[username]">&nbsp;Open and starred by me</option>
+ <option value="8" [is can "8"]selected=selected [define selected]True[end] [end]
+         title="[[]Open issues] commentby:[username]">&nbsp;Open with comment by me</option>
+[end]
+
+[# TODO(jrobbins): deprecate these and tell projects to define canned queries instead.]
+<option value="6" [is can "6"]selected=selected [define selected]True[end] [end]
+         title="[[]Open issues] status=New">&nbsp;New issues</option>
+<option value="7" [is can "7"]selected=selected [define selected]True[end] [end]
+         title="[[]All issues] status=fixed,done">&nbsp;Issues to verify</option>
+
+[is arg0 "admin"][else]
+  [define first]Yes[end]
+  [for canned_queries]
+    [is first "Yes"]
+      [define first]No[end]
+      <option disabled="disabled">----</option>
+    [end]
+    [# TODO(jrobbins): canned query visibility conditions, e.g., members only. ]
+    <option value="[canned_queries.query_id]"
+            [is can canned_queries.query_id]
+                selected=selected
+                [define selected]True[end]
+            [end]
+            title="[canned_queries.docstring]"
+            >&nbsp;[canned_queries.name]</option>
+  [end]
+  [if-any perms.EditProject][if-any is_cross_project][else]
+      [is first "Yes"]
+        [define first]No[end]
+        <option disabled="disabled">----</option>
+      [end]
+      <option value="manageprojectqueries"
+              >&nbsp;Manage project queries...</option>
+  [end][end]
+
+  [if-any logged_in_user]
+    [define first]Yes[end]
+    [for saved_queries]
+      [is first "Yes"]
+        [define first]No[end]
+        <option disabled="disabled">----</option>
+      [end]
+      <option value="[saved_queries.query_id]"
+              [is can saved_queries.query_id]
+                  selected=selected
+                  [define selected]True[end]
+              [end]
+              title="[saved_queries.docstring]"
+              >&nbsp;[saved_queries.name]</option>
+    [end]
+    [is first "Yes"]
+      [define first]No[end]
+      <option disabled="disabled">----</option>
+    [end]
+    <option value="managemyqueries"
+            >&nbsp;Manage my saved queries...</option>
+  [end][# end if logged in]
+
+[end][# end not "admin"]
+
+[is selected "False"]
+  <option value="[can]" selected=selected
+          title="Custom Query"
+          >&nbsp;Custom Query</option>
+[end]
diff --git a/templates/tracker/issue-change-notification-email-link-only.ezt b/templates/tracker/issue-change-notification-email-link-only.ezt
new file mode 100644
index 0000000..e0adf5c
--- /dev/null
+++ b/templates/tracker/issue-change-notification-email-link-only.ezt
@@ -0,0 +1,2 @@
+The following issue was [if-any was_created]created[else]updated[end]:
+[detail_url]
diff --git a/templates/tracker/issue-change-notification-email.ezt b/templates/tracker/issue-change-notification-email.ezt
new file mode 100644
index 0000000..a8b3a93
--- /dev/null
+++ b/templates/tracker/issue-change-notification-email.ezt
@@ -0,0 +1,37 @@
+[is comment.sequence "0"][#
+  ]Status: [is issue.status.name ""]----[else][issue.status.name][end]
+[#]Owner: [is issue.owner.username ""]----[else][issue.owner.display_name][end][#
+  ][if-any issue.cc]
+[#  ]CC: [for issue.cc][issue.cc.display_name][if-index issue.cc last] [else], [end][end][#
+  ][end][#
+  ][if-any issue.labels]
+[#  ]Labels:[for issue.labels] [issue.labels.name][end][#
+  ][end][#
+  ][if-any issue.components]
+[#  ]Components:[for issue.components] [issue.components.path][end][#
+  ][end][#
+  ][if-any issue.blocked_on]
+[#  ]BlockedOn:[for issue.blocked_on] [if-any issue.blocked_on.visible][issue.blocked_on.display_name][end][end][#
+  ][end][#
+  ][for issue.fields][if-any issue.fields.display][if-any issue.fields.values]
+[#  ][issue.fields.field_name]:[for issue.fields.values] [issue.fields.values.val][end][end][#
+  ][end][end]
+[else][if-any comment.amendments][#
+  ]Updates:
+[#][for comment.amendments]	[comment.amendments.field_name]: [format "raw"][comment.amendments.newvalue][end]
+[#][end][#
+  ][end][end]
+[is comment.sequence "0"]New issue [issue.local_id][#
+  ][else]Comment #[comment.sequence] on issue [issue.local_id][end][#
+  ] by [comment.creator.display_name]: [format "raw"][summary][#
+][end]
+[detail_url]
+
+[if-any comment.content][#
+  ][for comment.text_runs][include "render-plain-text.ezt" comment.text_runs][end][#
+][else](No comment was entered for this change.)[#
+][end]
+[if-any comment.attachments]
+Attachments:
+[for comment.attachments]	[comment.attachments.filename]  [comment.attachments.filesizestr]
+[end][end]
diff --git a/templates/tracker/issue-chart-body.ezt b/templates/tracker/issue-chart-body.ezt
new file mode 100644
index 0000000..34ae0f7
--- /dev/null
+++ b/templates/tracker/issue-chart-body.ezt
@@ -0,0 +1,17 @@
+<mr-chart
+  projectName="[projectname]"
+  hotlistId="[if-any hotlist_id][hotlist_id][end]"
+></mr-chart>
+
+<div>
+  <div class="help" style="padding: 1em;">
+    <h2 style="font-size: 1.2em; margin: 0 0 0.5em;">Supported query parameters:</h2>
+    <span style="font-family: monospace;">
+      cc, component, hotlist, label, owner, reporter, status
+    </span>
+    <br /><br />
+    <a href="https://bugs.chromium.org/p/monorail/issues/entry?labels=Feature-Charts">
+      Please file feedback here.
+    </a>
+  </div>
+</div>
diff --git a/templates/tracker/issue-chart-controls-top.ezt b/templates/tracker/issue-chart-controls-top.ezt
new file mode 100644
index 0000000..082affb
--- /dev/null
+++ b/templates/tracker/issue-chart-controls-top.ezt
@@ -0,0 +1,9 @@
+<!-- TODO: make this cleaner by replacing it with web component. -->
+<div class="list">
+  <div class="button_set">
+    <a class="choice_chip" href="[if-any is_hotlist][else]list[end]?can=[can]&amp;q=[query]&amp;colspec=[format "url"][colspec][end]&amp;groupby=[format "url"][groupby][end]&amp;sort=[format "url"][sortspec][end]&amp;x=[grid_x_attr]&amp;y=[grid_y_attr]&amp;cells=[grid_cell_mode]">List</a>
+    <a class="choice_chip" href="[if-any is_hotlist][else]list[end]?can=[can]&amp;q=[query]&amp;colspec=[format "url"][colspec][end]&amp;groupby=[format "url"][groupby][end]&amp;sort=[format "url"][sortspec][end]&amp;x=[grid_x_attr]&amp;y=[grid_y_attr]&amp;cells=[grid_cell_mode]&amp;mode=grid">Grid</a>
+    <a class="choice_chip active_choice" href="[if-any is_hotlist][else]list[end]?can=[can]&amp;q=[query]&amp;colspec=[format "url"][colspec][end]&amp;groupby=[format "url"][groupby][end]&amp;sort=[format "url"][sortspec][end]&amp;x=[grid_x_attr]&amp;y=[grid_y_attr]&amp;cells=[grid_cell_mode]&amp;mode=chart">Chart</a>
+  </div>
+</div>
+
diff --git a/templates/tracker/issue-entry-page.ezt b/templates/tracker/issue-entry-page.ezt
new file mode 100644
index 0000000..b9889d6
--- /dev/null
+++ b/templates/tracker/issue-entry-page.ezt
@@ -0,0 +1,556 @@
+[define title]New Issue[end]
+[define category_css]css/ph_detail.css[end]
+[include "../framework/header.ezt" "hidetabs"]
+
+[# Note: base permission for this page is CreateIssue]
+
+[if-any read_only][include "../framework/read-only-rejection.ezt"]
+[else]
+
+<div id="color_control" style="margin-top: 0; padding: 3px;" class="closed [if-any code_font]codefont[end]">
+ <form action="entry.do" method="POST" style="margin: 0; padding: 0" enctype="multipart/form-data" id="create_issue_form">
+  <input type="hidden" name="token" value="[form_token]">
+  <input type="hidden" name="template_name" value="[template_name]">
+  <input type="hidden" name="star" id="star_input" value="1">
+  <table cellpadding="0" cellspacing="0" border="0" role="presentation">
+   <tr><td>
+
+    <table cellspacing="0" cellpadding="3" border="0" class="rowmajor vt" role="presentation">
+     [if-any offer_templates]
+      <tr><th><label for="template_name">Template:</label></th>
+       <td colspan="2">
+        <select name="template_name" id="template_name" data-project-name="[projectname]" ignore-dirty>
+         [for config.template_names]
+           <option role="option" value="[format "url"][config.template_names][end]" [is config.template_names template_name]selected=selected[end]>[config.template_names]</option>
+         [end]
+        </select>
+        <span id="mr-code-font-toggle-slot"></span>
+       </td>
+      </tr>
+     [else]
+      <tr>
+       <td colspan="3">
+        <span id="mr-code-font-toggle-slot"></span>
+       </td>
+      </tr>
+     [end]
+
+      <tr><th><label for="summary">Summary:</label></th>
+       <td colspan="2" class="inplace">
+        <input type="text" id="summary" name="summary" value="[initial_summary]" required data-clear-summary-on-click="[clear_summary_on_click]"
+               [if-any any_errors][else]autofocus[end]>
+        [if-any errors.summary]
+         <div class="fielderror">[errors.summary]</div>
+        [end]
+
+        [if-any any_errors][else]
+          <script type="text/javascript" nonce="[nonce]">
+            document.getElementById('summary').select();
+          </script>
+        [end]
+       </td>
+      </tr>
+
+      <tr><th rowspan="3"><label for="comment">Description:</label></th>
+       <td colspan="2">
+        <textarea style="width:100%" cols="80" rows="15" name="comment" id="comment" class="issue_text" required>[initial_description]
+</textarea> [# We want 1 final newline but 0 trailing spaces in the textarea]
+        [if-any errors.comment]
+         <div class="fielderror">[errors.comment]</div>
+        [end]
+       </td>
+      </tr>
+
+      <tr><td colspan="2">
+       [include "../features/cues-conduct.ezt"]
+       <div id="attachmentareadeventry"></div>
+      </td></tr>
+
+      <tr>
+       <td style="width: 12em">
+        [if-any allow_attachments]
+         <span id="attachprompt"><img width="16" height="16" src="/static/images/paperclip.png" border="0" alt="A paperclip">
+         <a href="#" id="attachafile">Attach a file</a></span>
+         <div id="attachmaxsize" style="margin-left:1.2em; display:none">Max. attachments: [max_attach_size]</div>
+         [if-any errors.attachments]
+          <div class="fielderror">[errors.attachments]</div>
+         [end]
+        [else]
+         <div style="color:#666">Issue attachment storage quota exceeded.</div>
+        [end]
+       </td>
+       <td id="star_cell" style="vertical-align: initial">
+        [# Note: if the user is permitted to enter an issue, they are permitted to star it.]
+        <a class="star" id="star" style="color:cornflowerblue;">&#9733;</a>
+        Notify me of issue changes, if enabled in <a id="settings" target="new" href="/hosting/settings">settings</a>
+       </td>
+      </tr>
+
+      <tr [if-any page_perms.EditIssue page_perms.EditIssueStatus][else]style="display:none;"[end]><th width="10%"><label for="statusenter">Status:</label></th>
+       <td colspan="2" class="inplace">
+       <select id="statusenter" name="status">
+         <option style="display: none" value="[initial_status]"></option>
+       </select>
+       </label>
+       </td>
+      </tr>
+      <tr [if-any page_perms.EditIssue page_perms.EditIssueOwner][else]style="display:none;"[end]><th width="10%"><label for="ownerenter">Owner:</label></th>
+       <td colspan="2">
+        <input type="text" id="ownerenter" autocomplete="off"
+               style="width:16em"
+               name="owner" value="[initial_owner]" aria-autocomplete="list" role="combobox">
+          <span class="availability_[owner_avail_state]" id="owner_avail_state"
+                style="padding-left:1em; [if-any owner_avail_message_short][else]display:none[end]">
+            &#9608;
+            <span id="owner_availability">[owner_avail_message_short]</span>
+          </span>
+        </div>
+        [if-any errors.owner]
+         <div class="fielderror">[errors.owner]</div>
+        [end]
+       </td>
+      </tr>
+
+      <tr [if-any page_perms.EditIssue page_perms.EditIssueCc][else]style="display:none;"[end]><th><label for="memberenter">Cc:</label></th>
+       <td colspan="2" class="inplace">
+        <input type="text" multiple id="memberenter" autocomplete="off" name="cc" value="[initial_cc]" aria-autocomplete="list" role="combobox">
+        [if-any errors.cc]
+         <div class="fielderror">[errors.cc]</div>
+        [end]
+       </td>
+      </tr>
+
+      [# TODO(jrobbins): page_perms.EditIssueComponent]
+      <tr [if-any page_perms.EditIssue][else]style="display:none;"[end]><th><label for="components">Components:</label></th>
+       <td colspan="2" class="inplace">
+        <input type="text" id="components" autocomplete="off" name="components" value="[initial_components]" aria-autocomplete="list" role="combobox">
+        [if-any errors.components]
+         <div class="fielderror">[errors.components]</div>
+        [end]
+       </td>
+      </tr>
+
+      [if-any uneditable_fields]
+      <tr id="res_fd_banner"><th></th>
+        <td colspan="2" class="inplace" style="text-align:left; border-radius:25px">
+          <span style="background:var(--chops-orange-50); padding:5px; margin-top:10px; padding-left:10px; padding-right:10px; border-radius:25px">
+            <span style="padding-right:7px">
+              Info: Disabled inputs occur when you are not allowed to edit that restricted field.
+            </span>
+            <i id="res_fd_message" class="material-icons inline-icon" style="font-weight:bold; font-size:14px; vertical-align: text-bottom; cursor: pointer">
+            close</i>
+          </span>
+        </td>
+      </tr>
+      [end]
+
+      <tbody [if-any page_perms.EditIssue][else]style="display:none;"[end] class="collapse">
+       [define any_fields_to_reveal]No[end]
+       [for fields]
+        [if-any fields.applicable][if-any fields.field_def.is_approval_subfield][else][if-any fields.field_def.is_phase_field][else]
+         [# TODO(jrobbins): determine applicability dynamically and update fields in JS]
+         <tr [if-any fields.display][else]class="ifExpand"[define any_fields_to_reveal]Yes[end][end]>
+          <th id="[fields.field_id]_label">[fields.field_name]:</th>
+          <td colspan="2">
+            [if-any fields.is_editable]
+              [include "field-value-widgets.ezt" fields.field_def.is_multivalued_bool "" fields.field_def.is_required_bool ""]
+              <div class="fielderror" style="display:none" id="error_custom_[fields.field_id]"></div>
+            [else]
+              <input disabled value = "
+              [for fields.values]
+                [fields.values.val]
+              [end]
+              " style="text-align:right; width:12em" class="multivalued customfield" aria-labelledby="[fields.field_id]_label">
+            [end]
+          </td>
+         <tr>
+        [end][end][end]
+       [end]
+       [is any_fields_to_reveal "Yes"]
+        <tr class="ifCollapse">
+         <td colspan="2"><a href="#" class="toggleCollapse">Show all fields</a><t/td>
+        </tr>
+       [end]
+      </tbody>
+
+      <tr [if-any page_perms.EditIssue][else]style="display:none;"[end]><th>Labels:</th>[# aria-labels added in label-fields.ezt]
+       <td colspan="2" class="labelediting">
+        [include "label-fields.ezt" "just-two" ""]
+       </td>
+      </tr>
+
+      <tbody class="collapse">
+       [if-any page_perms.EditIssue]
+       <tr class="ifCollapse">
+        <td><a href="#" class="toggleCollapse">More options</a></td>
+       </tr>
+       [end]
+
+       <tr [if-any page_perms.EditIssue][else]style="display:none;"[end] class="ifExpand"><th style="white-space:nowrap"><label for="blocked_on">Blocked on:</label></th>
+        <td class="inplace" colspan="2">
+         <input type="text" name="blocked_on" id="blocked_on" value="[initial_blocked_on]">
+         [if-any errors.blocked_on]
+          <div class="fielderror">[errors.blocked_on]</div>
+         [end]
+        </td>
+       </tr>
+       <tr [if-any page_perms.EditIssue][else]style="display:none;"[end] class="ifExpand"><th><label for="blocking">Blocking:</label></th>
+        <td class="inplace" colspan="2">
+         <input type="text" name="blocking" id="blocking" value="[initial_blocking]" />
+         [if-any errors.blocking]
+          <div class="fielderror">[errors.blocking]</div>
+         [end]
+        </td>
+       </tr>
+
+       <tr [if-any page_perms.EditIssue][else]style="display:none;"[end] class="ifExpand"><th><label for="hotlistsenter">Hotlists:</label></th>
+        <td class="inplace" colspan="2">
+         <input type="text" name="hotlists" autocomplete="off" id="hotlistsenter" value="[initial_hotlists]" />
+         [if-any errors.hotlists]
+          <div class="fielderror">[errors.hotlists]</div>
+         [end]
+        </td>
+       </tr>
+     </tbody>
+
+     [if-any approvals]
+        <tr>
+          <th>Launch Gates:</th>
+          <td colspan="7">
+            [include "launch-gates-widget.ezt"]
+          </td>
+        </tr>
+     [end]
+
+     [for fields][if-any fields.applicable][if-any fields.field_def.is_approval_subfield]
+     <tr is="subfield-row">
+       <th>[fields.field_def.parent_approval_name] [fields.field_name]:</th>
+       <td colspan="2">
+         [if-any fields.is_editable]
+           [include "field-value-widgets.ezt" False "tmpl" False ""]
+           <div class="fielderror" style="display:none" id="error_custom_[fields.field_id]"></div>
+         [else]
+           <input disabled value = "
+           [for fields.values]
+             [fields.values.val]
+           [end]
+           " style="text-align:right; width:12em" class="multivalued customfield" aria-labelledby="[fields.field_id]_label">
+         [end]
+       </td>
+     </tr>
+    [end][end][end]
+
+    [for issue_phase_names]
+      [for fields]
+        [is fields.phase_name issue_phase_names]
+        <tr>
+          <th>[issue_phase_names].[fields.field_name]:</th>
+            <td colspan="2">
+              [if-any fields.is_editable]
+                [include "field-value-widgets.ezt" False "tmpl" False issue_phase_names]
+                <div class="fielderror" style="display:none" id="error_custom_[issue_phase_names]_[fields.field_id]"></div>
+              [else]
+                <input disabled value = "
+                [for fields.values]
+                  [fields.values.val]
+                [end]
+                " style="text-align:right; width:12em" class="multivalued customfield" aria-labelledby="[fields.field_id]_label">
+              [end]
+            </td>
+          </th>
+        </tr>
+      [end][end][end]
+
+     [include "../framework/label-validation-row.ezt"]
+     [include "../framework/component-validation-row.ezt"]
+    </table>
+
+    <div style="padding:6px">
+     <input type="submit" id="submit_btn" name="btn" value="Submit issue">
+     <input type="button" id="discard" name="nobtn" value="Discard">
+    </div>
+
+   </td>
+   </tr>
+  </table>
+ </form>
+</div>
+
+[include "../features/filterrules-preview.ezt"]
+
+<div style="margin-top:5em; margin-left: 8px;">
+  Problems submitting issues?
+  <a href="#" id="new-issue-feedback-link">
+    Send feedback
+  </a>
+</div>
+
+<div id="helparea"></div>
+
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+  window.getTSMonClient().recordIssueEntryTiming();
+
+  if ($('launch-gates-table')) {
+    $('launch-gates-table').classList.remove('hidden');
+  }
+
+  if ($("template_name")) {
+    $("template_name").addEventListener("change", function(event) {
+      _switchTemplate(event.target.getAttribute("data-project-name"),
+                      event.target.value)
+    });
+  }
+
+  if ($("summary")) {
+    var clearSummaryOnClick = $("summary").getAttribute("data-clear-summary-on-click");
+    if (clearSummaryOnClick) {
+      $("summary").addEventListener("keydown", function(event) {
+        _clearOnFirstEvent('[format "js"][initial_summary][end]');
+      });
+    }
+    $("summary").addEventListener("click", function(event) {
+      if (clearSummaryOnClick) {
+        _clearOnFirstEvent('[format "js"][initial_summary][end]');
+      }
+      checksubmit();
+    });
+    $("summary").addEventListener("focus", function(event) {
+      _acrob(null);
+      _acof(event);
+    });
+    $("summary").addEventListener("keyup", function(event) {
+      checksubmit();
+      return true;
+    });
+  }
+
+  if ($("settings")) {
+    $("settings").addEventListener("focus", function(event) {
+      _acrob(null);
+    });
+  }
+  if ($("statusenter")) {
+    _loadStatusSelect("[projectname]", "statusenter", "[initial_status]");
+    $("statusenter").addEventListener("focus", function(event) {
+      _acrob(null);
+    });
+  }
+  if($("res_fd_message")) {
+    $("res_fd_message").onclick = function(){
+      $("res_fd_banner").classList.add("hidden");
+    };
+  };
+
+  if ($("submit_btn")) {
+    $("submit_btn").addEventListener("focus", function(event) {
+      _acrob(null);
+    });
+    $("submit_btn").addEventListener("click", function(event) {
+      _acrob(null);
+      _trimCommas();
+      userMadeChanges = false;
+    });
+  }
+  if ($("discard")) {
+    $("discard").addEventListener("focus", function(event) {
+      _acrob(null);
+    });
+    $("discard").addEventListener("click", function(event) {
+      _acrob(null);
+      _confirmDiscardEntry(event.target);
+      event.preventDefault();
+    });
+  }
+  if ($("new-issue-feedback-link")) {
+    $("new-issue-feedback-link").addEventListener("click", function(event) {
+      userfeedback.api.startFeedback({
+          'productId': '5208992',  // Required.
+          'productVersion': '[app_version]'  // Optional.
+        });
+    })
+  }
+
+  window.allowSubmit = true;
+  $("create_issue_form").addEventListener("submit", function() {
+    if (allowSubmit) {
+      allowSubmit = false;
+      $("submit_btn").value = "Creating issue...";
+      $("submit_btn").disabled = "disabled";
+    }
+    else {
+      event.preventDefault();
+    }
+  });
+
+  var _blockIdsToListeners = [[]"blocked_on", "blocking", "hotlistsenter"];
+  for (var i = 0; i < _blockIdsToListeners.length; i++) {
+    var id = _blockIdsToListeners[[]i];
+    if ($(id)) {
+      $(id).addEventListener("focus", function(event) {
+        _acrob(null);
+        _acof(event);
+      });
+    }
+  }
+
+  var _idsToAddDefaultListeners = [[]"ownerenter", "memberenter", "components"];
+  for (var i = 0; i < _idsToAddDefaultListeners.length; i++) {
+    var id = _idsToAddDefaultListeners[[]i];
+    if ($(id)) {
+      $(id).addEventListener("focus", function(event) {
+        _acrob(null);
+        _acof(event);
+      });
+    }
+  }
+
+  var _elementsToAddPresubmit = document.querySelectorAll(
+      "#create_issue_form input, #create_issue_form select");
+  var debounced_presubmit = debounce(TKR_presubmit, 500);
+  for (var i = 0; i < _elementsToAddPresubmit.length; i++) {
+    var el = _elementsToAddPresubmit[[]i];
+    el.addEventListener("keyup", debounced_presubmit);
+    el.addEventListener("change", debounced_presubmit);
+  }
+  debounced_presubmit();
+
+  if ($("attachafile")) {
+    $("attachafile").addEventListener("click", function(event) {
+      _addAttachmentFields("attachmentareadeventry");
+      event.preventDefault();
+    });
+  }
+
+  document.addEventListener('keydown', function(event) {
+    if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) {
+      event.preventDefault();
+      $('submit_btn').click();
+    }
+  })
+
+  window.onsubmit = function() {
+    TKR_initialFormValues = TKR_currentFormValues();
+  };
+
+  window.onbeforeunload = function() {
+    if (TKR_isDirty()) {
+      // This message is ignored in recent versions of Chrome and Firefox.
+      return "You have unsaved changes. Leave this page and discard them?";
+    }
+  };
+
+  _lfidprefix = 'labelenter';
+  [if-any any_errors]
+   function _clearOnFirstEvent(){}
+  [end]
+
+  [if-any page_perms.EditIssue page_perms.EditIssueStatus page_perms.EditIssueOwner page_perms.EditIssueCc]
+    setTimeout(_forceProperTableWidth, 100);
+  [end]
+
+  [if-any page_perms.EditIssue]
+   _exposeExistingLabelFields();
+  [end]
+
+  var field_error;
+  [if-any  errors.custom_fields]
+    [for errors.custom_fields]
+      field_error = document.getElementById('error_custom_' + [errors.custom_fields.field_id]);
+      field_error.textContent = "[errors.custom_fields.message]";
+      field_error.style.display = "";
+    [end]
+  [end]
+
+
+
+function checksubmit() {
+  var restrict_to_known = [if-any restrict_to_known]true[else]false[end];
+  var confirmmsg = document.getElementById('confirmmsg');
+  var cg = document.getElementById('cg');
+  var label_blocksubmitmsg = document.getElementById('blocksubmitmsg');
+  var component_blocksubmitmsg = document.getElementById('component_blocksubmitmsg');
+
+  // Check for templates that require components.
+  var component_required = [if-any component_required]true[else]false[end];
+  var components = document.getElementById('components');
+  if (components && component_required && components.value == "") {
+    component_blocksubmitmsg.textContent = "You must specify a component for this template.";
+  } else {
+    component_blocksubmitmsg.textContent = "";
+  }
+
+  var submit = document.getElementById('submit_btn');
+  var summary = document.getElementById('summary');
+  if ((restrict_to_known && confirmmsg && confirmmsg.textContent) ||
+      (label_blocksubmitmsg && label_blocksubmitmsg.textContent) ||
+      (component_blocksubmitmsg && component_blocksubmitmsg.textContent) ||
+      (cg && cg.value == "") ||
+      (!allowSubmit) ||
+      (!summary.value [if-any must_edit_summary]|| summary.value == '[format "js"][template_summary][end]'[end])) {
+     submit.disabled='disabled';
+  } else {
+     submit.disabled='';
+  }
+}
+checksubmit();
+setInterval(checksubmit, 700); [# catch changes that were not keystrokes, e.g., paste menu item.]
+
+$("star").addEventListener("click", function (event) {
+    _TKR_toggleStarLocal($("star"), "star_input");
+});
+
+  const mrCodeFontToggle = document.createElement('mr-pref-toggle');
+  mrCodeFontToggle.style = 'float:right; margin: 3px;';
+  [if-any code_font]
+    mrCodeFontToggle.initialValue = true;
+  [end]
+  [if-any logged_in_user]
+    mrCodeFontToggle.userDisplayName = "[logged_in_user.email]";
+  [end]
+  mrCodeFontToggle.label = "Code";
+  mrCodeFontToggle.title = "Code font";
+  mrCodeFontToggle.prefName = "code_font";
+  $('mr-code-font-toggle-slot').appendChild(mrCodeFontToggle);
+  mrCodeFontToggle.fetchPrefs();
+  mrCodeFontToggle.addEventListener('font-toggle', function(e) {
+    const checked = e.detail.checked;
+    const ancestor = $('color_control');
+    if (ancestor) {
+      if (checked) {
+        ancestor.classList.add('codefont');
+      } else {
+        ancestor.classList.remove('codefont');
+      }
+    }
+  });
+
+
+});
+</script>
+
+<script type="text/javascript" defer src="/static/third_party/js/keys.js?version=[app_version]" nonce="[nonce]"></script>
+<script type="text/javascript" defer src="/static/third_party/js/skipper.js?version=[app_version]" nonce="[nonce]"></script>
+<script type="text/javascript" defer src="https://support.google.com/inapp/api.js" nonce="[nonce]"></script>
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+  _setupKibblesOnEntryPage('[project_home_url]/issues/list');
+});
+</script>
+
+[end]
+
+[include "field-value-widgets-js.ezt"]
+[include "../framework/footer.ezt"]
+
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+  if (typeof(ClientLogger) === "function") {
+    const l = new ClientLogger("issues");
+    l.logStart("new-issue", "user-time");
+    document.forms.create_issue_form.addEventListener('submit', function() {
+      l.logStart("new-issue", "server-time");
+    });
+  }
+});
+</script>
diff --git a/templates/tracker/issue-export-page.ezt b/templates/tracker/issue-export-page.ezt
new file mode 100644
index 0000000..c7892d7
--- /dev/null
+++ b/templates/tracker/issue-export-page.ezt
@@ -0,0 +1,39 @@
+[define category_css]css/ph_detail.css[end]
+[include "../framework/header.ezt" "showtabs"]
+
+<h3>Issue export</h3>
+
+<form action="export/json" method="GET">
+  [# We use xhr_token here because we are doing a GET on a JSON servlet.]
+  <input type="hidden" name="token" value="[xhr_token]">
+  <table cellpadding="3" class="rowmajor vt">
+    <tr>
+     <th>Format</th>
+     <td style="width:90%">JSON</td>
+   </tr>
+   <tr>
+     <select id="can" name="can">
+       [include "issue-can-widget.ezt" "search"]
+     </select>
+     <label for="searchq"> for </label>
+     <span id="qq"><input type="text" size="[q_field_size]" id="searchq" name="q"
+         value="[query]" autocomplete="off"></span>
+   </tr>
+   <tr>
+     <th>Start</th>
+     <td><input type="number" size="7" name="start" value="[initial_start]"></td>
+   </tr>
+   <tr>
+     <th>Num</th>
+     <td><input type="number" size="4" name="num" value="[initial_num]"></td>
+   </tr>
+   <tr>
+     <th></th>
+     <td><input type="submit" name="btn" value="Submit"></td>
+   </tr>
+ </table>
+</form>
+
+
+
+[include "../framework/footer.ezt"]
diff --git a/templates/tracker/issue-grid-body.ezt b/templates/tracker/issue-grid-body.ezt
new file mode 100644
index 0000000..94f08ea
--- /dev/null
+++ b/templates/tracker/issue-grid-body.ezt
@@ -0,0 +1,75 @@
+[if-any results]
+
+ [is grid_x_attr "--"][else]
+  <tr>
+   [is grid_y_attr "--"][else]<th>&nbsp;</th>[end]
+   [for grid_x_headings]
+    <th>[grid_x_headings]</th>
+   [end]
+  </tr>
+ [end]
+
+ [for grid_data]
+  <tr class="grid">
+   [is grid_y_attr "--"][else]<th>[grid_data.grid_y_heading]</th>[end]
+
+   [for grid_data.cells_in_row]
+    <td class="vt hoverTarget [is grid_cell_mode "tiles"][else]idcount[end]">
+     [for grid_data.cells_in_row.tiles]
+      [is grid_cell_mode "tiles"]
+       [include "issue-grid-tile.ezt" grid_data.cells_in_row.tiles.starred grid_data.cells_in_row.tiles.local_id grid_data.cells_in_row.tiles.status grid_data.cells_in_row.tiles.summary grid_data.cells_in_row.tiles.issue_url grid_data.cells_in_row.tiles.data_idx]
+      [end]
+      [is grid_cell_mode "ids"]
+       <a title="[grid_data.cells_in_row.tiles.summary]"
+          href=[grid_data.cells_in_row.tiles.issue_url] class="computehref" data-idx="[grid_data.cells_in_row.tiles.data_idx]">[if-any is_hotlist][grid_data.cells_in_row.tiles.issue_ref][else][grid_data.cells_in_row.tiles.local_id][end]</a>
+      [end]
+     [end]
+     [is grid_cell_mode "counts"]
+      [is grid_data.cells_in_row.count "0"]
+      [else]
+       [is grid_data.cells_in_row.count "1"]
+        <a href=[for grid_data.cells_in_row.tiles][grid_data.cells_in_row.tiles.issue_url][end]
+           >[grid_data.cells_in_row.count] item</a>
+       [else]
+        <a href="[if-any is_hotlist][else]list[end]?can=[can]&amp;q=[grid_data.cells_in_row.drill_down][query]">[grid_data.cells_in_row.count] items</a>
+       [end]
+      [end]
+
+     [end]
+    </td>
+   [end]
+  </tr>
+ [end]
+
+[else]
+
+ <tr>
+  <td colspan="40" class="id" style="cursor:default">
+   <div style="padding: 3em; text-align: center">
+    [if-any is_hotlist]
+     This hotlist currently has no issues.<br>
+     [if-any owner_permissions editor_permissions]
+      Select 'Add issues...' in the above 'Actions...' dropdown menu to add some.
+     [end]
+    [else]
+     [if-any project_has_any_issues]
+      Your search did not generate any results.  <br>
+      [is can "1"]
+       You may want to remove some terms from your query.<br>
+      [else]
+       You may want to try your search over <a href="list?can=1&amp;q=[query]&amp;x=[grid_x_attr]&amp;y=[grid_y_attr]&amp;mode=grid">all issues</a>.<br>
+      [end]
+     [else]
+      This project currently has no issues.<br>
+      [if-any page_perms.CreateIssue]
+       [if-any read_only][else]
+        You may want to enter a <a class="id" href="entry">new issue</a>.
+       [end]
+      [end]
+     [end]
+    [end]
+   </div>
+  </td>
+ </tr>
+
+[end]
diff --git a/templates/tracker/issue-grid-controls-top.ezt b/templates/tracker/issue-grid-controls-top.ezt
new file mode 100644
index 0000000..5d63e67
--- /dev/null
+++ b/templates/tracker/issue-grid-controls-top.ezt
@@ -0,0 +1,59 @@
+<div class="list">
+
+<div class="button_set">
+  <a class="choice_chip" href="[if-any is_hotlist][else]list[end]?can=[can]&amp;q=[query]&amp;colspec=[format "url"][colspec][end]&amp;groupby=[format "url"][groupby][end]&amp;sort=[format "url"][sortspec][end]&amp;x=[grid_x_attr]&amp;y=[grid_y_attr]&amp;cells=[grid_cell_mode]">List</a><span
+  class="choice_chip active_choice">Grid</span>
+  <a class="choice_chip" href="[if-any is_hotlist][else]list[end]?can=[can]&amp;q=[query]&amp;colspec=[format "url"][colspec][end]&amp;groupby=[format "url"][groupby][end]&amp;sort=[format "url"][sortspec][end]&amp;x=[grid_x_attr]&amp;y=[grid_y_attr]&amp;cells=[grid_cell_mode]&amp;mode=chart">Chart</a>
+</div>
+
+[if-any pagination]
+ [if-any pagination.visible]
+  <div class="pagination">
+   [is pagination.total_count "1"]
+    [pagination.total_count] issue shown
+   [else]
+    [if-any grid_limited][grid_shown] issues of [end]
+    [pagination.total_count] issues shown
+   [end]
+  </div>
+ [end]
+[end]
+
+ <form id="colspecform" action="[if-any is_hotlist][else]list[end]" method="GET" style="display:inline">
+  <input type="hidden" name="can" value="[can]">
+  <input type="hidden" name="q" value="[query]">
+  <input type="hidden" name="colspec" id="colspec" value="[colspec]">
+  <input type="hidden" name="sort" value="[sortspec]">
+  <input type="hidden" name="groupby" value="[groupby]">
+  <input type="hidden" name="mode" value="grid">
+<span>Rows:</span>
+<select name="y" class="drop-down-bub">
+ <option value="--" [if-any grid_y_attr][else]selected=selected[end]>None</option>
+ [for grid_axis_choices]
+  <option value="[grid_axis_choices]"
+          [is grid_axis_choices grid_y_attr]selected=selected[end]
+    >[grid_axis_choices]</option>
+ [end]
+</select>
+
+<span style="margin-left:.7em">Cols:</span>
+<select name="x" class="drop-down-bub">
+ <option value="--" [if-any grid_x_attr][else]selected=selected[end]>None</option>
+ [for grid_axis_choices]
+  <option value="[grid_axis_choices]"
+          [is grid_axis_choices grid_x_attr]selected=selected[end]
+    >[grid_axis_choices]</option>
+ [end]
+</select>
+
+<span style="margin-left:.7em">Cells:</span>
+<select name="cells" class="drop-down-bub">
+ <option value="tiles" [is grid_cell_mode "tiles"]selected=selected[end]>Tiles</option>
+ <option value="ids" [is grid_cell_mode "ids"]selected=selected[end]>IDs</option>
+ <option value="counts" [is grid_cell_mode "counts"]selected=selected[end]>Counts</option>
+</select>
+
+<input type="submit" name="nobtn" style="font-size:90%; margin-left:.5em" value="Update">
+
+</form>
+</div>
diff --git a/templates/tracker/issue-grid-tile.ezt b/templates/tracker/issue-grid-tile.ezt
new file mode 100644
index 0000000..43e6a04
--- /dev/null
+++ b/templates/tracker/issue-grid-tile.ezt
@@ -0,0 +1,27 @@
+<div class="gridtile">
+ <table cellspacing="0" cellpadding="0">
+  <tr>
+   <td class="id">
+    [if-any read_only][else]
+     [if-any page_perms.SetStar]
+      <a class="star"
+       style="color:[if-any arg0]cornflowerblue[else]gray[end]; text-decoration:none;"
+       title="[if-any arg0]Un-s[else]S[end]tar this issue"
+       data-project-name="[projectname]" data-local-id="[arg1]">
+       [if-any arg0]&#9733;[else]&#9734;[end]
+      </a>
+     [end]
+    [end]
+    <a href=[arg4] class="computehref" data-idx=[arg5]>[arg1]</a>
+   </td>
+   <td class="status">[arg2]</td>
+  </tr>
+  <tr>
+   <td colspan="2">
+    <div>
+     <a href=[arg4] class="computehref" data-idx=[arg5]>[arg3]</a>
+    </div>
+   </td>
+  </tr>
+ </table>
+</div>
diff --git a/templates/tracker/issue-hidden-fields.ezt b/templates/tracker/issue-hidden-fields.ezt
new file mode 100644
index 0000000..eaeeedf
--- /dev/null
+++ b/templates/tracker/issue-hidden-fields.ezt
@@ -0,0 +1,14 @@
+[# This template part renders important hidden fields for issue update forms.
+]
+
+<input type="hidden" name="_charset_" value="">
+<input type="hidden" name="token" value="[form_token]">
+<input type="hidden" name="id" value="[issue.local_id]">
+<input type="hidden" name="can" value="[can]">
+<input type="hidden" name="q" value="[query]">
+<input type="hidden" name="colspec" value="[colspec]">
+<input type="hidden" name="sort" value="[sortspec]">
+<input type="hidden" name="groupby" value="[groupby]">
+<input type="hidden" name="start" value="[start]">
+<input type="hidden" name="num" value="[num]">
+<input type="hidden" name="pagegen" value="[pagegen]">
diff --git a/templates/tracker/issue-hovercard.ezt b/templates/tracker/issue-hovercard.ezt
new file mode 100644
index 0000000..b70341b
--- /dev/null
+++ b/templates/tracker/issue-hovercard.ezt
@@ -0,0 +1,6 @@
+[# Show a small dialog box allows the user to quickly view one issue.]
+
+<div id="infobubble">
+ <div id="peekarea" style="width:72em; padding:0"
+      ><div class="loading">Loading...</div></div>
+</div>
diff --git a/templates/tracker/issue-import-page.ezt b/templates/tracker/issue-import-page.ezt
new file mode 100644
index 0000000..524486d
--- /dev/null
+++ b/templates/tracker/issue-import-page.ezt
@@ -0,0 +1,44 @@
+[define category_css]css/ph_detail.css[end]
+[include "../framework/header.ezt" "showtabs"]
+
+<h3>Issue export</h3>
+
+[if-any import_errors]
+  [# This is actually used to show both errors and progress messages
+     after a successful import.]
+  <div class="error" style="margin-bottom:1em">
+    Import event log:
+    <ul>
+      [for import_errors]
+        <li>[import_errors]</li>
+      [end]
+    </ul>
+  </div>
+[end]
+
+
+<form action="import.do" enctype="multipart/form-data" method="POST">
+  <input type="hidden" name="token" value="[form_token]">
+  <table cellpadding="3" class="rowmajor vt">
+    <tr>
+     <th>Format</th>
+     <td style="width:90%">JSON</td>
+   </tr>
+   <tr>
+     <th>File</th>
+     <td><input type="file" name="jsonfile"></td>
+   </tr>
+   <tr>
+     <th>Pre-check only</th>
+     <td><input type="checkbox" name="pre_check_only"></td>
+   </tr>
+   <tr>
+     <th></th>
+     <td><input type="submit" name="btn" value="Submit"></td>
+   </tr>
+ </table>
+</form>
+
+
+
+[include "../framework/footer.ezt"]
diff --git a/templates/tracker/issue-list-controls-bottom.ezt b/templates/tracker/issue-list-controls-bottom.ezt
new file mode 100644
index 0000000..b1905a1
--- /dev/null
+++ b/templates/tracker/issue-list-controls-bottom.ezt
@@ -0,0 +1,7 @@
+<div class="list-foot">
+[if-any logged_in_user]
+  <a href="[csv_link]&token=[form_token]" style="float:right; margin-left: 1em">CSV</a>
+[end]
+
+[include "../framework/artifact-list-pagination-part.ezt"]
+</div>
diff --git a/templates/tracker/issue-list-controls-top.ezt b/templates/tracker/issue-list-controls-top.ezt
new file mode 100644
index 0000000..d308570
--- /dev/null
+++ b/templates/tracker/issue-list-controls-top.ezt
@@ -0,0 +1,108 @@
+<div class="list">
+  <div class="button_set">
+   <span class="active_choice choice_chip">List</span>
+   <a class="choice_chip" href="[if-any is_hotlist][else]list[end]?can=[can]&amp;q=[query]&amp;colspec=[format "url"][colspec][end]&amp;groupby=[format "url"][groupby][end]&amp;sort=[format "url"][sortspec][end]&amp;x=[grid_x_attr]&amp;y=[grid_y_attr]&amp;cells=[grid_cell_mode]&amp;mode=grid">Grid</a>
+   <a class="choice_chip" href="[if-any is_hotlist][else]list[end]?can=[can]&amp;q=[query]&amp;colspec=[format "url"][colspec][end]&amp;groupby=[format "url"][groupby][end]&amp;sort=[format "url"][sortspec][end]&amp;x=[grid_x_attr]&amp;y=[grid_y_attr]&amp;cells=[grid_cell_mode]&amp;mode=chart">Chart</a>
+  </div>
+
+   [include "../framework/artifact-list-pagination-part.ezt"]
+   [include "update-issues-hotlists-dialog.ezt"]
+
+   [if-any page_perms.EditIssue]
+     [if-any is_cross_project][else]
+       <span style="margin:0 .7em">Select:
+         <a id="selectall" href="#">All</a>
+         <a id="selectnone" href="#">None</a>
+       </span>
+     [end]
+    <select id="moreactions" class="drop-down-bub">
+     <option value="moreactions" disabled="disabled" selected="selected">Actions...</option>
+     <option value="colspec">Change columns...</option>
+     [if-any is_cross_project][else][# TODO(jrobbins): cross-project bulk edit]
+       <option value="bulk">Bulk edit...</option>
+     [end]
+     [if-any is_cross_project][else][# TODO(jrobbins): cross-project spam flagging]
+       <option value="flagspam">Flag as spam...</option>
+       <option value="unflagspam">Un-flag as spam...</option>
+     [end]
+     <option value="addtohotlist">Add to hotlist...</option>
+    </select>
+    <span id='bulk-action-loading' class='loading' style='visibility:hidden'>Processing</span>
+   [end]
+
+   [if-any hotlist_id][if-any logged_in_user]
+   <span style="margin:0 .7em">Select:
+     <a id="selectall" href="#">All</a>
+     <a id="selectnone" href="#">None</a>
+   </span>
+   <select id="moreactions" class="drop-down-bub">
+     <option value="moreactions" disabled="disabled" [if-any add_issues_selected][else]selected="selected"[end]>Actions...</option>
+     [if-any owner_permissions editor_permissions]
+     <option value="addissues" [if-any add_issues_selected]selected="selected"[end]>Add issues...</option>
+     <option value="removeissues">Remove issues...</options>
+     <option value="colspec">Change columns...</option>
+   [end]
+     <option value="addtohotlist">Add to hotlist...</option>
+   </select>
+   [end][end]
+
+
+   <form id="colspecform" action=[if-any hotlist_id]"[hotlist.name]"[else]"list"[end] method="GET" autocomplete="off"
+         style="display:inline; margin-left:1em">
+    <input type="hidden" name="can" value="[can]">
+    <input type="hidden" name="q" value="[query]">
+    <input type="hidden" name="sort" value="[sortspec]">
+    <input type="hidden" id="groupbyspec" name="groupby" value="[groupby]">
+    <span id="columnspec" style="display:none; font-size:90%">
+      <span>Columns:</span>
+      <span id="colspec_field"><input type="text" size="60" name="colspec"
+                   value="[colspec]"></span>
+      <input type="submit" name="nobtn" value="Update">
+      [# TODO(jrobbins): <a href="TODO">Learn more</a> ]
+    </span>
+   </form>
+</div>
+
+[if-any is_hotlist]
+<form id='bulkremoveissues' method="POST" action="/u/[viewed_user_id]/hotlists/[hotlist.name].do">
+<input type="hidden" name="token" value="[edit_hotlist_token]">
+  <input type="hidden" id="current_col_spec" name="current_col_spec" value="[col_spec]">
+  <input type="hidden" id="bulk_remove_local_ids" name="remove_local_ids">
+  <input type ="hidden" id="bulk_remove_value" name = "remove" value="false">
+  <span id="addissuesspec" style="display:none; font-size:90%">
+    <span>Issues:</span>
+    <span id="issues_field"><input type="text" size="60" name="add_local_ids"
+                   value="[add_local_ids]" placeholder="[placeholder]"></span>
+    <input type="submit" name="nobtn" value="Add Issues">
+  </span>
+  [if-any errors.issues]
+  <div class="fielderror">[errors.issues]</div>
+  [end]
+  <div class="fielderror">&nbsp;
+    <span id="add_local_idsfeedback">
+       [if-any errors.add_local_ids][errors.add_local_ids][end]
+    </span>
+  </div>
+</form>
+[end]
+
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+  if ($("selectall")) {
+    $("selectall").addEventListener("click", function() { _selectAllIssues(); });
+  }
+  if ($("selectnone")) {
+    $("selectnone").addEventListener("click", function() { _selectNoneIssues(); });
+  }
+  if ($("moreactions")) {
+    $("moreactions").addEventListener("change", function(event) {
+        _handleListActions(event.target);
+    });
+    if ($("moreactions").value == 'addissues') {
+      _showID('addissuesspec');
+    }
+  }
+  window.__hotlists_dialog.onResponse = onAddIssuesResponse;
+  window.__hotlists_dialog.onFailure = onAddIssuesFailure;
+});
+</script>
diff --git a/templates/tracker/issue-list-csv.ezt b/templates/tracker/issue-list-csv.ezt
new file mode 100644
index 0000000..62de9c6
--- /dev/null
+++ b/templates/tracker/issue-list-csv.ezt
@@ -0,0 +1,16 @@
+[# Prefix response body with over 1024 bytes of static content to avoid content sniffing.]
+"-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-"
+This file contains the same information as the issue list web page, but in CSV format.
+You can adjust the columns of the CSV file by adjusting the columns shown on the web page
+before clicking the CSV link.
+"-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-"
+
+
+[for panels][# There will always be exactly one panel.][for panels.ordered_columns]"[panels.ordered_columns.name]"[if-index panels.ordered_columns last][else],[end][end][end]
+[for table_data][for table_data.cells][is table_data.cells.type "ID"]"[table_data.local_id]",[else]"[format "raw"][if-any table_data.cells.values][for table_data.cells.values][is table_data.cells.type "issues"][table_data.cells.values.item.id][else][table_data.cells.values.item][end][if-index table_data.cells.values last][else], [end][end][end][end]"[if-index table_data.cells last][else],[end][end][end]
+[end]
+
+[if-any next_csv_link]
+This file is truncated to [item_count] out of [pagination.total_count] total results.
+See [next_csv_link] for the next set of results.
+[end]
diff --git a/templates/tracker/issue-list-headings.ezt b/templates/tracker/issue-list-headings.ezt
new file mode 100644
index 0000000..6ec4e41
--- /dev/null
+++ b/templates/tracker/issue-list-headings.ezt
@@ -0,0 +1,31 @@
+[# arg0 is the ordered_columns argument that gives the name and index of each column.]
+
+<thead id="resultstablehead">
+<tr id="headingrow"><th style="border-left: 0"> &nbsp; </th>
+ [for panels.ordered_columns]
+  [is panels.ordered_columns.name "Summary"]
+   <th class="col_[panels.ordered_columns.col_index]" nowrap="nowrap" id="summaryheading"
+       data-col-index="[panels.ordered_columns.col_index]" width="100%"
+       ><a href="#" style="text-decoration: none">Summary + Labels <span class="indicator">&#9660;</span></a></th>
+  [else]
+   [is panels.ordered_columns.name "ID"]
+    <th class="col_[panels.ordered_columns.col_index]" nowrap="nowrap"
+        data-col-index="[panels.ordered_columns.col_index]"
+       ><a href="#" style="text-decoration: none">[panels.ordered_columns.name] <span class="indicator">&#9660;</span></a></th>
+   [else]
+    <th class="col_[panels.ordered_columns.col_index]"
+        data-col-index="[panels.ordered_columns.col_index]"
+       ><a href="#" style="text-decoration: none">[panels.ordered_columns.name]&nbsp;<span class="indicator">&#9660;</span></a></th>
+   [end]
+  [end]
+ [end]
+ [if-any is_hotlist]
+ <th data-col-index="dot" style="width:3ex"><a href="#columnprefs"
+     class="dotdotdot" aria-label="Column list">...</a></th>
+ [else]
+ <th style="padding: 0;">
+   <ezt-show-columns-connector colspec="[colspec]" phasespec="[phasespec]"></ezt-show-columns-connector>
+ </th>
+ [end]
+</tr>
+</thead>
diff --git a/templates/tracker/issue-list-js.ezt b/templates/tracker/issue-list-js.ezt
new file mode 100644
index 0000000..1eb1aad
--- /dev/null
+++ b/templates/tracker/issue-list-js.ezt
@@ -0,0 +1,110 @@
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+
+  [# Pass the list of column names from HTML to JS ]
+  window._allColumnNames = [
+    [for column_values]'[column_values.column_name]'[if-index column_values last][else], [end][end]
+    ];
+
+  [# Update the issue link hrefs on-load and whenever the column-spec changes.]
+  _ctxCan = [can];
+  _ctxQuery = "[format "js"][query][end]";
+  _ctxSortspec = "[format "js"][sortspec][end]";
+  _ctxGroupBy = "[format "js"][groupby][end]";
+  _ctxDefaultColspec = "[format "js"][default_colspec][end]";
+  _ctxStart = [start];
+  _ctxNum = [num];
+  _ctxResultsPerPage = [default_results_per_page];
+  _ctxHotlistID = "[hotlist_id]";
+  _ctxArgs = _formatContextQueryArgs();
+
+  function _goIssue(issueIndex, newWindow) {
+    var url = _makeIssueLink(issueRefs[[]issueIndex]);
+    _go(url, newWindow);
+  }
+  // Added to enable calling from TKR_openArtifactAtCursor
+  window._goIssue = _goIssue;
+
+  window.issueRefs = [[]
+   [for table_data]
+     {project_name: "[format "js"][table_data.project_name][end]",
+      id: [table_data.local_id]}[if-index table_data last][else],[end][end]
+   ];
+
+  function _handleResultsClick(event) {
+    var target = event.target;
+    if (event.button >= 3)
+      return;
+    if (target.classList.contains("label"))
+      return;
+    if (target.classList.contains("rowwidgets") || target.parentNode.classList.contains("rowwidgets"))
+      return;
+    while (target && target.tagName != "TR") target = target.parentNode;
+    if ('[is_hotlist]') {
+       if (!target.attributes[[]"issue-context-url"]) return;
+       _go(target.attributes[[]"issue-context-url"].value, (event.metaKey || event.ctrlKey || event.button == 1));
+       }
+    else {
+       if (!target.attributes[[]"data-idx"]) return;
+       _goIssue(target.attributes[[]"data-idx"].value,
+       (event.metaKey || event.ctrlKey || event.button == 1));
+         }
+  };
+  [if-any table_data]
+    _addClickListener($("resultstable"), _handleResultsClick);
+  [end]
+
+  var issueCheckboxes = document.getElementsByClassName("checkRangeSelect");
+  for (var i = 0; i < issueCheckboxes.length; ++i) {
+    var el = issueCheckboxes[[]i];
+    el.addEventListener("click", function (event) {
+        _checkRangeSelect(event, event.target);
+        _highlightRow(event.target);
+    });
+  }
+
+  function _handleHeaderClick(event) {
+    var target = event.target;
+    while (target && target.tagName != "TH") target = target.parentNode;
+    var colIndex = target.getAttribute("data-col-index");
+    if (colIndex) {
+      _showBelow("pop_" + colIndex, target);
+    }
+    event.preventDefault();
+  }
+  var resultsTableHead = $("resultstablehead");
+  if (resultsTableHead) {
+    resultsTableHead.addEventListener("click", _handleHeaderClick);
+  }
+
+  if (typeof(ClientLogger) == "function") {
+    let cl = new ClientLogger("issues");
+    if (cl.started("issue-search")) {
+      cl.logPause("issue-search", "computer-time");
+      cl.logResume("issue-search", "user-time");
+
+      // Now we want to listen for clicks on any issue search result.
+      let logResultClick = function() {
+        cl.logPause("issue-search", "user-time");
+        cl.logResume("issue-search", "computer-time");
+      }
+
+      let links = document.querySelectorAll("#resultstable tbody .id a");
+      for (let i = 0; i < links.length; i++) {
+        links[[]i].addEventListener("click", logResultClick);
+      }
+    }
+  }
+});
+</script>
+
+<script type="text/javascript" defer src="/static/third_party/js/keys.js?version=[app_version]" nonce="[nonce]"></script>
+<script type="text/javascript" defer src="/static/third_party/js/skipper.js?version=[app_version]" nonce="[nonce]"></script>
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+  _setupKibblesOnListPage(
+    [is arg0 "issuelist"]'[project_home_url]/issues/list'[else]'[currentPageURLEncoded]'[end],
+    '[project_home_url]/issues/entry',
+    '[projectname]', [is arg0 "issuelist"]1[else]5[end], 0);
+});
+</script>
diff --git a/templates/tracker/issue-list-menus.ezt b/templates/tracker/issue-list-menus.ezt
new file mode 100644
index 0000000..d4ab7a5
--- /dev/null
+++ b/templates/tracker/issue-list-menus.ezt
@@ -0,0 +1,170 @@
+[# Table header popup menus ]
+
+[for column_values]
+ [is column_values.column_name "id"]
+   <div id="pop_[column_values.col_index]" class="popup">
+    <table cellspacing="0" cellpadding="0" border="0">
+     <tr id="pop_up_[column_values.col_index]"><td>Sort Up</td></tr>
+     <tr id="pop_down_[column_values.col_index]"><td>Sort Down</td></tr>
+     <tr id="pop_hide_[column_values.col_index]"><td>Hide Column</td></tr>
+    </table>
+   </div>
+ [else]
+  [is column_values.column_name "summary"]
+   <div id="pop_[column_values.col_index]" class="popup">
+    <table cellspacing="0" cellpadding="0" border="0">
+     <tr id="pop_up_[column_values.col_index]"><td>Sort Up</td></tr>
+     <tr id="pop_down_[column_values.col_index]"><td>Sort Down</td></tr>
+     [if-any is_hotlist][else]
+     [if-any column_values.filter_values]
+      <tr id="pop_show_only_[column_values.col_index]"><td>Show only
+          <span class="indicator">&#9658;</span></td></tr>
+     [end][end]
+     <tr id="pop_hide_[column_values.col_index]"><td>Hide Column</td></tr>
+    </table>
+   </div>
+  [else]
+   <div id="pop_[column_values.col_index]" class="popup">
+    <table cellspacing="0" cellpadding="0" border="0">
+     <tr id="pop_up_[column_values.col_index]"><td>Sort Up</td></tr>
+     <tr id="pop_down_[column_values.col_index]"><td>Sort Down</td></tr>
+     [if-any is_hotlist][else]
+     [if-any column_values.filter_values]
+      <tr id="pop_show_only_[column_values.col_index]"><td>Show only
+          <span class="indicator">&#9658;</span></td></tr>
+     [end][end]
+     <tr id="pop_hide_[column_values.col_index]"><td>Hide Column</td></tr>
+     <tr id="pop_groupby_[column_values.col_index]"><td>Group Rows</td></tr>
+    </table>
+   </div>
+  [end]
+ [end]
+[end]
+
+[# Table header popup submenus for autofiltering of values ]
+
+[for column_values]
+ <div id="filter_[column_values.col_index]" class="popup subpopup">
+  <table cellspacing="0" cellpadding="0" border="0">
+   [for column_values.filter_values]
+    <tr data-filter-column="[is column_values.column_name "Summary"]label[else][column_values.column_name][end]"
+        data-filter-value="[column_values.filter_values]">
+     <td>[column_values.filter_values]</td></tr>
+   [end]
+  </table>
+ </div>
+[end]
+
+[# Popup menu showing the list of available columns allowing show/hide ]
+
+<div id="pop_dot" class="popup">
+ <table cellspacing="0" cellpadding="0" border="0">
+  <tr><th>Show columns:</th></tr>
+   [for panels.ordered_columns]
+    <tr data-toggle-column-index="[panels.ordered_columns.col_index]"><td>&nbsp;<span
+        class="col_[panels.ordered_columns.col_index]">&diams;</span>&nbsp;[panels.ordered_columns.name]</td></tr>
+   [end]
+   [for unshown_columns]
+    <tr data-add-column-name="[unshown_columns]"
+        ><td>&nbsp;&nbsp;&nbsp;&nbsp;[unshown_columns]</td></tr>
+   [end]
+   <tr id="pop_dot_edit"
+      ><td>&nbsp;&nbsp;&nbsp;&nbsp;Edit&nbsp;column&nbsp;spec...</td></tr>
+ </table>
+</div>
+
+
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+  function registerPopHandlers(colIndex, colName) {
+    var sortUpEl = $("pop_up_" + colIndex);
+    if (sortUpEl) {
+      sortUpEl.addEventListener("click", function () {
+        _closeAllPopups(sortUpEl);
+        _sortUp(colName);
+      });
+      sortUpEl.addEventListener("mouseover", function () {
+       _closeSubmenus();
+      });
+    }
+
+    var sortDownEl = $("pop_down_" + colIndex);
+    if (sortDownEl) {
+      sortDownEl.addEventListener("click", function () {
+        _closeAllPopups(sortDownEl);
+        _sortDown(colName);
+      });
+      sortDownEl.addEventListener("mouseover", function () {
+       _closeSubmenus();
+      });
+    }
+
+    var hideEl = $("pop_hide_" + colIndex);
+    if (hideEl) {
+      hideEl.addEventListener("click", function () {
+        _closeAllPopups(hideEl);
+        _toggleColumnUpdate(colIndex);
+      });
+      hideEl.addEventListener("mouseover", function () {
+       _closeSubmenus();
+      });
+    }
+
+    var showOnlyEl = $("pop_show_only_" + colIndex);
+    if (showOnlyEl) {
+      showOnlyEl.addEventListener("mouseover", function () {
+        _showRight("filter_" + colIndex, showOnlyEl);
+      });
+    }
+
+    var groupByEl = $("pop_groupby_" + colIndex);
+    if (groupByEl) {
+      groupByEl.addEventListener("click", function () {
+        _closeAllPopups(groupByEl);
+        _addGroupBy(colIndex);
+      });
+      groupByEl.addEventListener("mouseover", function () {
+       _closeSubmenus();
+      });
+    }
+  }
+
+  [for column_values]
+    registerPopHandlers([column_values.col_index], "[column_values.column_name]");
+  [end]
+
+  function handleFilterValueClick(event) {
+    var target = event.target;
+    if (target.tagName != "TR") target = target.parentNode;
+    _closeAllPopups(target);
+    var filterColumn = target.getAttribute("data-filter-column");
+    var filterValue = target.getAttribute("data-filter-value");
+    _filterTo(filterColumn, filterValue);
+  }
+
+  [for column_values]
+    $("filter_" + [column_values.col_index]).addEventListener(
+        "click", handleFilterValueClick);
+  [end]
+
+  function handleDotDotDotClick(event) {
+    var target = event.target;
+    if (target.tagName != "TR") target = target.parentNode;
+    _closeAllPopups(target);
+    var colIndex = target.getAttribute("data-toggle-column-index");
+    if (colIndex != null)
+      _toggleColumnUpdate(colIndex);
+    var colName = target.getAttribute("data-add-column-name");
+    if (colName != null)
+      _addcol(colName);
+  }
+
+  $("pop_dot").addEventListener("click", handleDotDotDotClick);
+
+  $("pop_dot_edit").addEventListener("click", function() {
+    var target = $("pop_dot_edit");
+    _closeAllPopups(target);
+    $("columnspec").style.display = "";
+  });
+});
+</script>
diff --git a/templates/tracker/issue-original-page.ezt b/templates/tracker/issue-original-page.ezt
new file mode 100644
index 0000000..580b739
--- /dev/null
+++ b/templates/tracker/issue-original-page.ezt
@@ -0,0 +1,17 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+<html>
+ <head>
+ <title>Issue [projectname]:[local_id] comment #[seq]</title>
+ <meta name="ROBOTS" content="NOINDEX">
+ <meta name="referrer" content="no-referrer">
+ <link type="text/css" rel="stylesheet" href="[version_base]/static/css/ph_core.css">
+ </head>
+ <body>
+  <h3>Original email for issue [projectname]:[local_id] comment #[seq]</h3>
+  [if-any is_binary]
+   <i>The message could not be displayed.</i>
+  [else]
+   <pre>[message_body]</pre>
+  [end]
+ </body>
+</html>
diff --git a/templates/tracker/issue-reindex-page.ezt b/templates/tracker/issue-reindex-page.ezt
new file mode 100644
index 0000000..0d80dee
--- /dev/null
+++ b/templates/tracker/issue-reindex-page.ezt
@@ -0,0 +1,45 @@
+[define title]Reindex Issues[end]
+[define category_css]css/ph_detail.css[end]
+[include "../framework/header.ezt" "showtabs"]
+
+[if-any read_only][include "../framework/read-only-rejection.ezt"]
+[else]
+
+<form action="reindex.do" method="POST" id="form">
+  <input type="hidden" name="token" value="[form_token]">
+  <table>
+    <tr>
+      <td>Start:</td>
+      <td><input type="input" name="start" value="[start]"></td>
+    </tr>
+    <tr>
+      <td>Num:</td>
+      <td><input type="input" name="num" value="[num]"></td>
+    </tr>
+    <tr>
+      <td colspan="2">
+        <input type="submit" id="submit_btn" name="btn" value="Re-index"></td>
+    </tr>
+    <tr>
+      <td><label for="autosubmit">Autosubmit:</label></td>
+      <td><input type="checkbox" name="auto_submit" id="autosubmit"
+                 [is auto_submit "True"]checked="checked"[end] ></td>
+    </tr>
+  </table>
+</form>
+
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+  function autosubmit() {
+    if (document.getElementById('autosubmit').checked) {
+      document.getElementById('form').submit();
+    }
+  }
+  if (document.getElementById('autosubmit').checked) {
+    setTimeout(autosubmit, 5000);
+  }
+});
+</script>
+
+[end]
+[include "../framework/footer.ezt"]
diff --git a/templates/tracker/issue-search-tips.ezt b/templates/tracker/issue-search-tips.ezt
new file mode 100644
index 0000000..50d4d8c
--- /dev/null
+++ b/templates/tracker/issue-search-tips.ezt
@@ -0,0 +1,398 @@
+[define category_css]css/ph_detail.css[end]
+[include "../framework/header.ezt" "hidetabs"]
+
+[# Note: No UI element permission checking needed on this page. ]
+
+<div id="searchtips">
+
+<h3>Basic issue search</h3>
+
+<p>In most cases you can find the issues that you want to work with
+very easily by using the issue list headers or by entering a few
+simple keywords into the main search field.</p>
+
+<p>Whenever you visit the "<a href="list">issue list</a>" in your
+project, you are presented with a table of all open issues, or the default
+query set up by the project owners.  If you
+see too many results, you can quickly filter your results by clicking
+on the table headers and choosing a specific value from the "Show
+only:" submenu.</p>
+
+[# TODO screenshot ]
+
+<p>The main search field consists of two parts:</p>
+
+<ul>
+ <li>A drop-down selection of search scopes, e.g, "All issues" or just "Open issues".</li>
+ <li>A search text field where you can enter search terms.</li>
+</ul>
+
+[# TODO screenshot ]
+
+<p>In the text field, you may enter simple search terms, or add any of
+the search operators described below.</p>
+
+<p>You can also use the search text field to jump directly to any
+issue by entering its issue number.  If you wish to search for issues
+that contain a number, rather than jumping to that issue, enclose the
+number in quotation marks.</p>
+
+<p>Behind the scenes, the search scope is simply an additional set of
+search terms that is automatically combined with the user's search
+terms to make a complete query.  To see what search terms will be
+used for each scope, hover your mouse over the scope item.</p>
+
+
+<h3>Advanced issue search</h3>
+
+<p>The <a href="advsearch">Advanced Search</a> page helps you
+compose a complex query.  The advanced search form breaks the search
+down into several popular criteria and allows you to specify each one
+easily.  The search criteria boil down to the same thing as the search
+operators described below, but you don't need to remember the operator
+names.</p>
+
+
+
+<h3>Full-text search</h3>
+
+<p>As with Google web search, you can search for issues by simply
+entering a few words.  However, you may get a few more results than
+you expected.  When you need to search more precisely, you can use
+search operators for more power.</p>
+
+<form action="list" method="GET">
+ <p><input type="text" size="45" name="q" value="&quot;out of memory&quot;">
+ <input type="submit" name="btn" value="Search"></p>
+</form>
+
+<p>Full-text search terms can include quoted phrases, and words or
+phrases can be negated by using a leading minus sign.  Please note
+that negated full-text terms are likely to give large result sets,
+so it is best to use structured search operators when possible.</p>
+
+
+<h3>Search operators</h3>
+
+<p>Normal search terms will match words found in any field of an
+issue.  You can narrow the search to a specific field by using the
+name of the field.  The built-in field operators are <tt>summary</tt>,
+<tt>description</tt>, <tt>comment</tt>, <tt>status</tt>, <tt>reporter</tt>,
+<tt>owner</tt>, <tt>cc</tt>, <tt>component</tt>, <tt>commentby</tt>,
+<tt>hotlist</tt>, <tt>ID</tt>, <tt>project</tt>,
+and <tt>label</tt>.</p>
+
+<p>Field names can be compared to a list of values using:</p>
+<ul>
+  <li>a colon (:) for word matching,</li>
+  <li>an equals sign (=) for full string matching,</li>
+  <li>a not equals sign (!=) or leading minus sign to negate, or</li>
+  <li>inequality operators (&lt;, &gt;, &lt;=, &gt;=) for numeric comparison.</li>
+</ul>
+
+<p>You can limit your search to just open issues by using
+is:open, or to just closed issues by using a minus sign to negate it:
+<tt>-is:open</tt>.</p>
+[# TODO(jrobbins): dateopened:]
+
+<p>For example, here's how to search for issues with the word
+"calculation" in the summary field.</p>
+
+<form action="list" method="GET">
+ <p><input type="text" size="45" name="q" value="summary:calculation">
+ <input type="submit" name="btn" value="Search"></p>
+</form>
+
+<p>When searching for issues owned by a specific user, you can use their
+email address, or part of it.  When referring to yourself, you can also
+ use the special term <tt>me</tt>. For example, this restricts the search to
+issues that are owned by you.</p>
+
+<form action="list" method="GET">
+ <p><input type="text" size="45" name="q" value="owner:user@chromium.org">
+ <input type="submit" name="btn" value="Search">
+</form>
+
+<form action="list" method="GET">
+ <p><input type="text" size="45" name="q" value="owner:me">
+ <input type="submit" name="btn" [if-any logged_in_user][else]disabled=disabled[end] value="Search">
+ [if-any logged_in_user][else]
+   <span style="white-space:nowrap"><a href="[login_url]"
+   >Sign in</a> to try this example</span>
+ [end]</p>
+</form>
+
+<p>Rather than have a large number of predefined fields, our issue
+tracker stores many issue details as labels.</p>
+
+<p>For example, if you labeled security-related issues with the label
+<tt>Security</tt>, here's how to search for them.</p>
+
+<form action="list" method="GET">
+ <p><input type="text" size="45" name="q" value="label:security">
+ <input type="submit" name="btn" value="Search"></p>
+</form>
+
+
+<p>In addition to simple one-word labels, you can use two part labels
+that specify an attribute and a value, like <tt>Priority-High</tt>,
+<tt>Priority-Medium</tt>, and <tt>Priority-Low</tt>.  You can search for
+these with the <tt>label</tt> operator, or you can use the first part of the
+label name like an operator.</p>
+
+<p>For example, if you labeled high priority issues with
+<tt>Priority-High</tt>, here's one way to search for them.</p>
+
+<form action="list" method="GET">
+ <p><input type="text" size="45" name="q" value="label:Priority-High">
+ <input type="submit" name="btn" value="Search"></p>
+</form>
+
+<p>And, here is a more compact way to do the same search.</p>
+
+<form action="list" method="GET">
+ <p><input type="text" size="45" name="q" value="Priority:High">
+ <input type="submit" name="btn" value="Search"></p>
+</form>
+
+<p>For the <tt>components</tt> operator, the default search will find
+issues in that component and all of its subcomponents.</p>
+
+<form action="list" method="GET">
+ <p><input type="text" size="45" name="q" value="component:UI">
+ <input type="submit" name="btn" value="Search"></p>
+</form>
+
+<p>And of course, you can combine any of these field operators.</p>
+
+<form action="list" method="GET">
+ <p><input type="text" size="45" name="q"
+     value="status!=New owner:me component:UI">
+ <input type="submit" name="btn" value="Search"></p>
+</form>
+
+<p>You can search for issues in the current project that are also on a user's
+hotlist.</p>
+
+<form action="list" method="GET">
+ <p><input type="text" size="45" name="q"
+     value="hostlist=username@domain:hotlistname">
+ <input type="submit" name="btn" value="Search"></p>
+</form>
+
+
+<h3>Empty (or non-empty) field search</h3>
+
+<p>For each built-in field operator, you can use the <tt>has</tt>
+operator to search for issues with empty or non-empty fields.  The
+<tt>has</tt> operator can be used with status, owner, cc, component,
+attachments, blocking, blockedon, mergedinto, any key-value label prefix, or
+any custom field name.</p>
+
+<p>For example, here's how to search for issues that have one or more
+components.</p>
+
+<form action="list" method="GET">
+ <p><input type="text" size="45" name="q" value="has:component">
+ <input type="submit" name="btn" value="Search">
+</form>
+
+<p>Or, you can use the <tt>-has</tt> operator for negation, to search for
+issues with empty fields.</p>
+
+<p>For example, here's how to search for issues that are not associated with
+any component.</p>
+
+<form action="list" method="GET">
+ <p><input type="text" size="45" name="q" value="-has:component">
+ <input type="submit" name="btn" value="Search">
+</form>
+
+
+<h3>Multiple values in search terms</h3>
+
+<p>You can search for two values for one field, or two labels
+with the same prefix by using.</p>
+
+<form action="list" method="GET">
+ <p><input type="text" size="45" name="q" value="Priority:High,Medium">
+ <input type="submit" name="btn" value="Search"></p>
+</form>
+
+
+<p>You can combine two separate queries into one using the <tt>OR</tt> operator.</p>
+
+<form action="list" method="GET">
+ <p><input type="text" size="45" name="q" value="Priority:High OR -has:owner">
+ <input type="submit" name="btn" value="Search"></p>
+</form>
+
+
+<p>You can create more complex <tt>OR</tt> queries using parentheses nesting to
+distribute search terms across <tt>OR</tt> clauses. A search query may contain as
+many sets of parentheses and levels of parentheses nesting as needed.</p>
+
+<form action="list" method="GET">
+ <p><input type="text" size="45" name="q" value="Pri:0,1 (status:Untriaged OR -has:owner)">
+ <input type="submit" name="btn" value="Search"></p>
+</form>
+
+
+<h3>Exact value search</h3>
+
+<p>You can search for issues that exactly match the given term by using
+the search operator <tt>=</tt>.</p>
+
+<p>For example, searching for <tt>Milestone=2009</tt> only matches issues with the
+label <tt>Milestone-2009</tt>, while searching for <tt>Milestone:2009</tt> matches
+issues with the labels <tt>Milestone-2009</tt>, <tt>Milestone-2009-Q1</tt>, <tt>Milestone-2009-Q3</tt>,
+etc.</p>
+
+<form action="list" method="GET">
+ <p><input type="text" size="45" name="q" value="Milestone=2009">
+ <input type="submit" name="btn" value="Search"></p>
+</form>
+
+<p>Similarly, using exact matching on components will get you only those issues
+that are in that component, not including any of its descendants.</p>
+
+<form action="list" method="GET">
+ <p><input type="text" size="45" name="q" value="component=UI">
+ <input type="submit" name="btn" value="Search"></p>
+</form>
+
+<h3>Star search</h3>
+
+<p>Any logged in user can mark any issue with a star.  The star
+indicates interest in the issue.</p>
+
+<p>For example, to quickly see all the issues in this project that you
+have starred, you could use the following:</p>
+
+<form action="list" method="GET">
+ <p><input type="text" size="45" name="q" value="is:starred">
+ <input type="submit" name="btn" [if-any logged_in_user][else]disabled="disabled"[end] value="Search">
+ [if-any logged_in_user][else]
+   <span style="white-space:nowrap"><a href="[login_url]"
+   >Sign in</a> to try this example</span>
+ [end]</p>
+</form>
+
+<p>And, to see the issues that more than ten users have starred, use the following:</p>
+
+<form action="list" method="GET">
+ <p><input type="text" size="45" name="q" value="stars>10">
+ <input type="submit" name="btn" value="Search"></p>
+</form>
+
+
+<h3>Jump to issue and numeric search</h3>
+
+<p>You can jump directly to a specific issue by entering its ID in the
+search field.</p>
+
+<p>For example, to jump to issue 1, just search for 1.  If there is no
+existing issue with that ID, the system will search for issues that
+contain that number anywhere in the issue.</p>
+
+<form action="list" method="GET">
+ <p><input type="text" size="45" name="q" value="1">
+ <input type="submit" name="btn" value="Search"></p>
+</form>
+
+<p>If you just want to search for issues that contain the number 1, without
+jumping to issue 1, enclose the number in quotation marks.</p>
+
+<form action="list" method="GET">
+ <p><input type="text" size="45" name="q" value="&quot;1&quot;">
+ <input type="submit" name="btn" value="Search"></p>
+</form>
+
+<p>Searching for a list of specific issue IDs is one way to
+communicate a set of issues to someone that you are working with.  Be
+sure to set the search scope to "All issues" if the issues might be
+closed.</p>
+
+<form action="list" method="GET">
+ <p><input type="text" size="45" name="q" value="ID=1,2,3,4">
+ <input type="hidden" name="can" value="1">
+ <input type="submit" name="btn" value="Search"></p>
+</form>
+
+
+<h3>Attachment search</h3>
+
+<p>Users can attach files to any issues, either when issues are created or as
+part of issue comments.</p>
+
+<p>To quickly see all the issues that have attachments, use the following:</p>
+
+<form action="list" method="GET">
+ <p><input type="text" size="45" name="q" value="has:attachments">
+ <input type="submit" name="btn" value="Search"></p>
+</form>
+
+<p>Or, you can search for a specific filename of the attachment.</p>
+
+<form action="list" method="GET">
+ <p><input type="text" size="45" name="q" value="attachment:screenshot">
+ <input type="submit" name="btn" value="Search"></p>
+</form>
+
+<p>You can also search for the file extension of the attachment.</p>
+
+<form action="list" method="GET">
+ <p><input type="text" size="45" name="q" value="attachment:png">
+ <input type="submit" name="btn" value="Search"></p>
+</form>
+
+
+<p>You can also search for issues with a certain number of  attachments.</p>
+
+<form action="list" method="GET">
+ <p><input type="text" size="45" name="q" value="attachments>10">
+ <input type="submit" name="btn" value="Search"></p>
+</form>
+
+
+<h3>Date range search</h3>
+
+<p>You can perform searches based on date ranges.</p>
+
+<p>This search syntax is divided into two parts, the action and the date,
+[[]action]:[[]date]</p>
+
+<p>Built-in date operators include <tt>opened</tt>,
+<tt>modified</tt>, and <tt>closed</tt>. Each can be paired with an
+inequality operator <tt>&lt</tt> or <tt>&gt</tt>. The date must to be
+specified as YYYY-MM-DD, YYYY/MM/DD or today-N.</p>
+
+<p>For example, if you want to search for issues opened after 2009/4/1, you
+could do the following:</p>
+
+<form action="list" method="GET">
+ <p><input type="text" size="45" name="q" value="opened>2009/4/1">
+ <input type="submit" name="btn" value="Search"></p>
+</form>
+
+<p>Or, if you want to search for issues modified 20 days before today's date,
+you could do the following:</p>
+
+<form action="list" method="GET">
+ <p><input type="text" size="45" name="q" value="modified<today-20">
+ <input type="submit" name="btn" value="Search"></p>
+</form>
+
+<p>You can search for issues that had specific fields modified
+recently by using ownermodified:, statusmodified:, componentmodified:.
+For example:</p>
+
+<form action="list" method="GET">
+ <p><input type="text" size="45" name="q" value="ownermodified>today-20">
+ <input type="submit" name="btn" value="Search"></p>
+</form>
+
+
+</div>
+
+[include "../framework/footer.ezt"]
diff --git a/templates/tracker/label-fields.ezt b/templates/tracker/label-fields.ezt
new file mode 100644
index 0000000..7909c3e
--- /dev/null
+++ b/templates/tracker/label-fields.ezt
@@ -0,0 +1,128 @@
+[# Make a 3x8 grid of label entry form fields with autocomplete on each one.
+
+   Args:
+     arg0: if "just-two" is passed, only show the first two rows
+         and give the user links to click to expose more rows.
+     arg1: the ID prefix for the row divs.
+]
+
+<div id="[arg1]LF_row1" class="nowrap">
+ <input aria-label="label 1" type="text" class="labelinput" id="[arg1]label0" size="20" autocomplete="off"
+        name="label" value="[label0]">
+ <input aria-label="label 2" type="text" class="labelinput" id="[arg1]label1" size="20" autocomplete="off"
+        name="label" value="[label1]">
+ <input aria-label="label 3" type="text" class="labelinput" id="[arg1]label2" size="20" autocomplete="off"
+        name="label" value="[label2]">
+</div>
+
+<div id="[arg1]LF_row2" class="nowrap">
+ <input aria-label="label 4" type="text" class="labelinput" id="[arg1]label3" size="20" autocomplete="off"
+        name="label" value="[label3]">
+ <input aria-label="label 5" type="text" class="labelinput" id="[arg1]label4" size="20" autocomplete="off"
+        name="label" value="[label4]">
+ <input aria-label="label 6" type="text" class="labelinput" id="[arg1]label5" size="20" autocomplete="off"
+        [is arg0 "just-two"]data-show-id="LF_row3" data-hide-id="addrow2"[end]
+        name="label" value="[label5]">
+ [is arg0 "just-two"]<span id="addrow2" class="fakelink" data-instead="LF_row3">Add a row</span>[end]
+</div>
+
+<div id="[arg1]LF_row3" [is arg0 "just-two"]style="display:none"[end] class="nowrap">
+ <input aria-label="label 7" type="text" class="labelinput" id="[arg1]label6" size="20" autocomplete="off"
+        name="label" value="[label6]">
+ <input aria-label="label 8" type="text" class="labelinput" id="[arg1]label7" size="20" autocomplete="off"
+        name="label" value="[label7]">
+ <input aria-label="label 9" type="text" class="labelinput" id="[arg1]label8" size="20" autocomplete="off"
+        [is arg0 "just-two"]data-show-id="LF_row4" data-hide-id="addrow3"[end]
+        name="label" value="[label8]">
+ [is arg0 "just-two"]<span id="addrow3" class="fakelink" data-instead="LF_row4">Add a row</span>[end]
+</div>
+
+<div id="[arg1]LF_row4" [is arg0 "just-two"]style="display:none"[end] class="nowrap">
+ <input aria-label="label 10" type="text" class="labelinput" id="[arg1]label9" size="20" autocomplete="off"
+        name="label" value="[label9]">
+ <input aria-label="label 11" type="text" class="labelinput" id="[arg1]label10" size="20" autocomplete="off"
+        name="label" value="[label10]">
+ <input aria-label="label 12" type="text" class="labelinput" id="[arg1]label11" size="20" autocomplete="off"
+        [is arg0 "just-two"]data-show-id="LF_row5" data-hide-id="addrow4"[end]
+        name="label" value="[label11]">
+ [is arg0 "just-two"]<span id="addrow4" class="fakelink" data-instead="LF_row5">Add a row</span>[end]
+</div>
+
+<div id="[arg1]LF_row5" [is arg0 "just-two"]style="display:none"[end] class="nowrap">
+ <input aria-label="label 13" type="text" class="labelinput" id="[arg1]label12" size="20" autocomplete="off"
+        name="label" value="[label12]">
+ <input aria-label="label 14" type="text" class="labelinput" id="[arg1]label13" size="20" autocomplete="off"
+        name="label" value="[label13]">
+ <input aria-label="label 15" type="text" class="labelinput" id="[arg1]label14" size="20" autocomplete="off"
+        [is arg0 "just-two"]data-show-id="LF_row6" data-hide-id="addrow5"[end]
+        name="label" value="[label14]">
+ [is arg0 "just-two"]<span id="addrow5" class="fakelink" data-instead="LF_row6">Add a row</span>[end]
+</div>
+
+<div id="[arg1]LF_row6" [is arg0 "just-two"]style="display:none"[end] class="nowrap">
+ <input aria-label="label 16" type="text" class="labelinput" id="[arg1]label15" size="20" autocomplete="off"
+        name="label" value="[label15]">
+ <input aria-label="label 17" type="text" class="labelinput" id="[arg1]label16" size="20" autocomplete="off"
+        name="label" value="[label16]">
+ <input aria-label="label 18" type="text" class="labelinput" id="[arg1]label17" size="20" autocomplete="off"
+        [is arg0 "just-two"]data-show-id="LF_row7" data-hide-id="addrow6"[end]
+        name="label" value="[label17]">
+ [is arg0 "just-two"]<span id="addrow6" class="fakelink" data-instead="LF_row7">Add a row</span>[end]
+</div>
+
+<div id="[arg1]LF_row7" [is arg0 "just-two"]style="display:none"[end] class="nowrap">
+ <input aria-label="label 19" type="text" class="labelinput" id="[arg1]label18" size="20" autocomplete="off"
+        name="label" value="[label18]">
+ <input aria-label="label 20" type="text" class="labelinput" id="[arg1]label19" size="20" autocomplete="off"
+        name="label" value="[label19]">
+ <input aria-label="label 21" type="text" class="labelinput" id="[arg1]label20" size="20" autocomplete="off"
+        [is arg0 "just-two"]data-show-id="LF_row8" data-hide-id="addrow7"[end]
+        name="label" value="[label20]">
+ [is arg0 "just-two"]<span id="addrow7" class="fakelink" data-instead="LF_row8">Add a row</span>[end]
+</div>
+
+<div id="[arg1]LF_row8" [is arg0 "just-two"]style="display:none"[end] class="nowrap">
+ <input aria-label="label 22" type="text" class="labelinput" id="[arg1]label21" size="20" autocomplete="off"
+        name="label" value="[label21]">
+ <input aria-label="label 23" type="text" class="labelinput" id="[arg1]label22" size="20" autocomplete="off"
+        name="label" value="[label22]">
+ <input aria-label="label 24" type="text" class="labelinput" id="[arg1]label23" size="20" autocomplete="off"
+        name="label" value="[label23]">
+</div>
+
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+  var labelInputs = document.getElementsByClassName("labelinput");
+  for (var i = 0; i < labelInputs.length; ++i) {
+    var labelInput = labelInputs[[]i];
+    if (labelInput.getAttribute("id").startsWith("hidden")) continue;
+    labelInput.addEventListener("keyup", function (event) {
+        if (event.target.getAttribute("data-show-id") &&
+            event.target.getAttribute("data-hide-id") &&
+            event.target.value) {
+          _showID(event.target.getAttribute("data-show-id"));
+          _hideID(event.target.getAttribute("data-hide-id"));
+        }
+        return _vallab(event.target);
+    });
+    labelInput.addEventListener("blur", function (event) {
+        _acrob(null);
+        return _vallab(event.target);
+    });
+    labelInput.addEventListener("focus", function (event) {
+        return _acof(event);
+    });
+  }
+
+  var addRowLinks = document.getElementsByClassName("fakelink");
+  for (var i = 0; i < addRowLinks.length; ++i) {
+    var rowLink = addRowLinks[[]i];
+    rowLink.addEventListener("click", function (event) {
+        _acrob(null);
+        var insteadID = event.target.getAttribute("data-instead");
+        if (insteadID)
+          _showInstead(insteadID, this);
+    });
+  }
+});
+</script>
diff --git a/templates/tracker/launch-gates-widget.ezt b/templates/tracker/launch-gates-widget.ezt
new file mode 100644
index 0000000..225a3d2
--- /dev/null
+++ b/templates/tracker/launch-gates-widget.ezt
@@ -0,0 +1,50 @@
+<table id="launch-gates-table" class="hidden">
+  <tr>
+    <th>Approval</th>
+    <th style="color:grey">gate-less</th>
+    <th><input name="phase_0" placeholder="Gate Name" size="7" [if-any allow_edit][else]disabled[end]></th>
+    <th><input name="phase_1" size="7" [if-any allow_edit][else]disabled[end]></th>
+    <th><input name="phase_2" size="7" [if-any allow_edit][else]disabled[end]></th>
+    <th><input name="phase_3" size="7" [if-any allow_edit][else]disabled[end]></th>
+    <th><input name="phase_4" size="7" [if-any allow_edit][else]disabled[end]></th>
+    <th><input name="phase_5" size="7" [if-any allow_edit][else]disabled[end]></th>
+    <th style="color:grey">omit</th>
+  </tr>
+  [for approvals]
+    <tr>
+      <td nowrap><b>[approvals.field_name]</b>
+        <br>
+        <span><input id="[approvals.field_id]_required" name="approval_[approvals.field_id]_required" type="checkbox" [if-any allow_edit][else]disabled[end]>
+        <label for="[approvals.field_id]_required">Require review</label></span>
+      </td>
+      <td><input id="[approvals.field_id]" name="approval_[approvals.field_id]" value="no_phase" type="radio" [if-any allow_edit][else]disabled[end]></td>
+      <td><input id="[approvals.field_id]_phase_0" name="approval_[approvals.field_id]" value="phase_0" type="radio" [if-any allow_edit][else]disabled[end]></td>
+      <td><input id="[approvals.field_id]_phase_1" name="approval_[approvals.field_id]" value="phase_1" type="radio" [if-any allow_edit][else]disabled[end]></td>
+      <td><input id="[approvals.field_id]_phase_2" name="approval_[approvals.field_id]" value="phase_2" type="radio" [if-any allow_edit][else]disabled[end]></td>
+      <td><input id="[approvals.field_id]_phase_3" name="approval_[approvals.field_id]" value="phase_3" type="radio" [if-any allow_edit][else]disabled[end]></td>
+      <td><input id="[approvals.field_id]_phase_4" name="approval_[approvals.field_id]" value="phase_4" type="radio" [if-any allow_edit][else]disabled[end]></td>
+      <td><input id="[approvals.field_id]_phase_5" name="approval_[approvals.field_id]" value="phase_5" type="radio" [if-any allow_edit][else]disabled[end]></td>
+      <td><input name="approval_[approvals.field_id]" value="omit" type="radio" checked="checked" [if-any allow_edit][else]disabled[end]></td>
+    </tr>
+  [end]
+</table>
+
+
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+  let phaseNum = 0;
+  [for initial_phases]
+    document.getElementsByName(`phase_${phaseNum++}`)[0].value = '[format "js"][initial_phases.name][end]';
+  [end]
+
+  [for prechecked_approvals]
+    document.getElementById("[prechecked_approvals]").checked = "checked"
+  [end]
+
+  [for required_approval_ids]
+    document.getElementById("[required_approval_ids]_required").checked = "checked"
+  [end]
+
+});
+
+</script>
\ No newline at end of file
diff --git a/templates/tracker/render-plain-text.ezt b/templates/tracker/render-plain-text.ezt
new file mode 100644
index 0000000..e79bb2a
--- /dev/null
+++ b/templates/tracker/render-plain-text.ezt
@@ -0,0 +1,8 @@
+[# Safely display some text that includes some markup, completely removing the markup.
+
+  arg0 is a list of element EZT objects that have a tad and content and maybe some
+  other attributes.
+
+  We do not use extra whitespace in this template because it generates text into a
+  context where whitespace is significant.
+][arg0.content]
\ No newline at end of file
diff --git a/templates/tracker/render-rich-text.ezt b/templates/tracker/render-rich-text.ezt
new file mode 100644
index 0000000..89ab07d
--- /dev/null
+++ b/templates/tracker/render-rich-text.ezt
@@ -0,0 +1,10 @@
+[# Safely display some text that includes some markup.  Only the tags
+   that we explicitly allowlist are allowed, everything else gets
+   escaped.
+
+   description.text_runs is a list of element EZT objects that have a
+   tag and content and maybe some other attributes.
+
+   We do not use extra whitespace in this template because it
+   generates text into a context where whitespace is significant.
+][is arg0.tag ""][arg0.content][end][is arg0.tag "a"]<a href="[arg0.href]" title="[arg0.title]" class="[arg0.css_class]" rel="nofollow">[arg0.content]</a>[end][is arg0.tag "b"]<b>[arg0.content]</b>[end]
\ No newline at end of file
diff --git a/templates/tracker/spam-moderation-queue.ezt b/templates/tracker/spam-moderation-queue.ezt
new file mode 100644
index 0000000..e43e477
--- /dev/null
+++ b/templates/tracker/spam-moderation-queue.ezt
@@ -0,0 +1,121 @@
+[define title]Spam Moderation Queue[end]
+[define category_css]css/ph_list.css[end]
+[define page_css]css/ph_detail.css[end][# needed for infopeek]
+
+[if-any projectname]
+  [include "../framework/header.ezt" "showtabs"]
+[else]
+  [include "../framework/header.ezt" "hidetabs"]
+[end]
+[include "../framework/js-placeholders.ezt" "showtabs"]
+
+<h2>Spam Moderation Queue: Automatic Classifier Close Calls</h2>
+[include "../framework/artifact-list-pagination-part.ezt"]
+
+<button type="submit" vaue="mark_spam" disabled="true">Mark as Spam</button>
+<button type="submit" value="mark_ham" disabled="true">Mark as Ham</button>
+
+<span style="margin:0 .7em">Select:
+  <a id="selectall" href="#">All</a>
+  <a id="selectnone" href="#">None</a>
+</span>
+
+<table id='resultstable'>
+<tr>
+  <td>
+  </td>
+  <td>ID</td>
+  <td>Author</td>
+  <td>Summary</td>
+  <td>Snippet</td>
+  <td>Opened at</td>
+  <td>Spam?</td>
+  <td>Verdict reason</td>
+  <td>Confidence</td>
+  <td>Verdict at</td>
+  <td>Flag count</td>
+</tr>
+[for issue_queue]
+<tr>
+  <td><input type='checkbox' name='issue_local_id' value='[issue_queue.issue.local_id]'/></td>
+  <td><a href='/p/[projectname]/issues/detail?id=[issue_queue.issue.local_id]'>[issue_queue.issue.local_id]</a></td>
+  <td><a href='/u/[issue_queue.reporter.email]'>[issue_queue.reporter.email]</a></td>
+  <td><a href='/p/[projectname]/issues/detail?id=[issue_queue.issue.local_id]'>[issue_queue.summary]</a></td>
+  <td>
+  [issue_queue.comment_text]
+  </td>
+  <td>[issue_queue.issue.opened_timestamp]</td>
+  <td>[issue_queue.issue.is_spam]</td>
+
+  <td>[issue_queue.reason]</td>
+  <td>[issue_queue.classifier_confidence]</td>
+  <td>[issue_queue.verdict_time]</td>
+  <td>[issue_queue.flag_count]</td>
+</tr>
+[end]
+</table>
+
+[include "../framework/artifact-list-pagination-part.ezt"]
+<button type="submit" vaue="mark_spam" disabled="true">Mark as Spam</button>
+<button type="submit" value="mark_ham" disabled="true">Mark as Ham</button>
+
+</form>
+
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+  if ($("selectall")) {
+    $("selectall").addEventListener("click", function() {
+        _selectAllIssues();
+        setDisabled(false);
+    });
+  }
+  if ($("selectnone")) {
+    $("selectnone").addEventListener("click", function() {
+        _selectNoneIssues();
+        setDisabled(true);
+    });
+  }
+
+  const checkboxes = Array.from(
+      document.querySelectorAll('input[type=checkbox]'));
+  checkboxes.forEach(checkbox => {
+    checkbox.addEventListener('change', updateEnabled);
+  });
+
+  const buttons = Array.from(
+      document.querySelectorAll('button[type=submit]'));
+  buttons.forEach(button => {
+    button.addEventListener('click', function(event) {
+      const markSpam = (button.value === 'mark_spam');
+      const issueRefs = [];
+      checkboxes.forEach(checkbox => {
+        if (checkbox.checked) {
+          issueRefs.push({
+              projectName: window.CS_env.projectName,
+              localId: checkbox.value,
+          });
+          const rowElement = checkbox.parentElement.parentElement;
+          rowElement.parentElement.removeChild(rowElement);
+        }
+      });
+      window.prpcClient.call('monorail.Issues', 'FlagIssues', {
+        issueRefs: issueRefs,
+        flag: markSpam,
+      });
+    });
+  });
+
+  function updateEnabled() {
+    const anySelected = checkboxes.some(checkbox => checkbox.checked);
+    setDisabled(!anySelected);
+   }
+
+  function setDisabled(disabled) {
+    buttons.forEach(button => {
+      button.disabled = disabled;
+    });
+  }
+});
+</script>
+
+[include "../framework/footer.ezt"]
diff --git a/templates/tracker/template-detail-page.ezt b/templates/tracker/template-detail-page.ezt
new file mode 100644
index 0000000..b596e4c
--- /dev/null
+++ b/templates/tracker/template-detail-page.ezt
@@ -0,0 +1,303 @@
+[define title]Issue Template [template_name][end]
+[define category_css]css/ph_detail.css[end]
+[include "../framework/header.ezt" "showtabs"]
+
+<a href="/p/[projectname]/adminTemplates">&lsaquo; Back to template list</a><br><br>
+
+[if-any read_only][include "../framework/read-only-rejection.ezt"]
+[else]
+
+<h4>Issue Template</h4>
+
+[if-any new_template_form]
+  <form action="create.do" method="POST">
+[else]
+  <form action="detail.do" method="POST">
+[end]
+<input type="hidden" name="token" value="[form_token]">
+<input type="hidden" name="template" value="[template_name]">
+
+
+<table cellspacing="0" cellpadding="3" class="rowmajor vt">
+  <tr>
+    <th>Members only:</th>
+    <td>
+      <input type="checkbox"[if-any allow_edit][else]disabled[end] name="members_only" [if-any initial_members_only]checked[end]>
+      <label for="members_only_checkbox">Only offer this template to project members</label>
+    </td>
+  </tr>
+
+  <tr>
+    <th>Name:</th>
+    <td>
+      [if-any new_template_form]
+        <input type="text" name="name" value="[template_name]">
+        <span id="fieldnamefeedback" class="fielderror" style="margin-left:1em">
+          [if-any errors.name][errors.name][end]
+        </span>
+      [else]
+        [template_name]
+        <input type="hidden" name="name" value="[template_name]">
+      [end]
+    </td>
+  </tr>
+
+  <tr>
+    <th>Summary:</th>
+    <td>
+      [if-any allow_edit]
+        <input type="text" name="summary" size="60" class=acob" value="[initial_summary]"><br>
+      [else]
+        [initial_summary]<br>
+      [end]
+      <input type="checkbox" [if-any allow_edit][else]disabled[end] name="summary_must_be_edited" [if-any initial_must_edit_summary]checked[end]>
+      <label for="summary_must_be_edited_checkbox">Users must edit issue summary before submitting</label>
+    </td>
+  </tr>
+
+  <tr>
+    <th>Description:</th>
+    <td>
+      [if-any allow_edit]
+         <textarea name="content" rows="12" cols="75">[initial_content]</textarea>
+         [# Note: wrap="hard" has no effect on content_editor because we copy to a hidden field before submission.]
+      [else]
+        [initial_content]
+      [end]
+    </td>
+  </tr>
+
+  <tr>
+    <th>Status:</th>
+    <td>
+      [if-any allow_edit]
+        <select id="status" name="status">
+          <option style="display: none" value="[initial_status]"></option>
+        </select>
+      [else]
+        [initial_status]
+      [end]
+    </td>
+  </tr>
+
+  <tr>
+    <th>Owner:</th>
+    <td>
+       [if-any allow_edit]
+         <input id="owner_editor" type="text" name="owner" size="25" class="acob" value="[initial_owner]"
+                autocomplete="off">
+         <span id="fieldnamefeedback" class="fielderror" style="margin-left:1em">
+            [if-any errors.owner][errors.owner][end]
+         </span>
+       [else]
+         [initial_owner]<br>
+       [end]
+       <span>
+        <input type="checkbox" [if-any allow_edit][else]disabled[end] name="owner_defaults_to_member" style="margin-left:2em" [if-any initial_owner_defaults_to_member]checked[end]>
+        <label for="owner_defaults_to_member_checkbox">Default to member who is entering the issue</label>
+       </span>
+    </td>
+  </tr>
+
+  <tr>
+    <th>Components:</th>
+    <td>
+      [if-any allow_edit]
+        <input id="components" type="text" name="components" size="75" class="acob"
+               autocomplete="off" value="[initial_components]">
+       <span id="fieldnamefeedback" class="fielderror" style="margin-left:1em">
+          [if-any errors.components][errors.components][end]
+       </span>
+      [else]
+        [initial_components]
+      [end]
+       <br/>
+       <span>
+        <input type="checkbox" [if-any allow_edit][else]disabled[end] name="component_required" [if-any initial_component_required]checked[end]>
+        <label for="component_required_checkbox">Require at least one component</label>
+       </span>
+    </td>
+  </tr>
+
+  [if-any allow_edit][if-any uneditable_fields]
+  <tr id="res_fd_banner"><th></th>
+    <td style="text-align:left; border-radius:25px">
+      <span style="background:var(--chops-orange-50); padding:5px; margin-top:10px;padding-left:10px; padding-right:10px; border-radius:25px">
+        <span style="padding-right:7px">
+        Info: Disabled inputs occur when you are not allowed to edit that restricted field.
+        </span>
+        <i id="res_fd_message" class="material-icons inline-icon" style="font-weight:bold; font-size:14px; vertical-align: text-bottom; cursor: pointer">
+        close</i>
+      </span>
+    </td>
+  </tr>
+  [end][end]
+
+  [for fields]
+    [# TODO(jrobbins): determine applicability dynamically and update fields in JS]
+    [# approval subfields are shown below, not here]
+    [if-any fields.field_def.is_approval_subfield][else][if-any fields.field_def.is_phase_field][else]
+      <tr>
+        <th>[fields.field_name]:</th>
+        <td colspan="2">
+          [if-any allow_edit]
+            [if-any fields.is_editable]
+              [include "field-value-widgets.ezt" False "tmpl" False ""]
+            [else]
+              <input disabled value = "
+              [for fields.values]
+                [fields.values.val]
+              [end]
+              " style="text-align:right; width:12em" class="multivalued customfield" aria-labelledby="[fields.field_id]_label">
+            [end]
+          [else]
+            [for fields.values]
+              [fields.values.val]
+            [end]
+          [end]
+        </td>
+      <tr>
+    [end][end]
+  [end]
+
+  <tr>
+    <th>Labels:</th>
+    <td>
+      [include "label-fields.ezt" "all" ""]
+     </td>
+   </tr>
+
+   <tr>
+     <th>Template admins:</th>
+     <td>
+       [if-any allow_edit]
+         <input id="admin_names_editor" type="text" name="admin_names" size="75" class="acob" value="[initial_admins]"
+                autocomplete="off">
+       [else]
+         [initial_admins]
+       [end]
+     </td>
+   </tr>
+
+  [if-any approvals]
+     <tr>
+       <th>Launch Gates:</th>
+       <td colspan="7">
+         <input type="checkbox" name="add_approvals" id="cb_add_approvals" [if-any allow_edit][else]disabled[end] [if-any initial_add_approvals]checked="checked"[end]>
+         <label for="cb_add_approvals">Include Gates and Approval Tasks in issue</label>
+         [include "launch-gates-widget.ezt"]
+         <span id="fieldnamefeedback" class="fielderror" style="margin-left:1em">
+              [if-any errors.phase_approvals][errors.phase_approvals][end]
+         </span>
+       </td>
+     </tr>
+  [end]
+
+  [for fields]
+    [if-any fields.field_def.is_approval_subfield]
+      <tr id="subfield-row" class="subfield-row-class">
+        <th>[fields.field_def.parent_approval_name] [fields.field_name]:</th>
+        <td colspan="2">
+          [if-any allow_edit]
+            [if-any fields.is_editable]
+              [include "field-value-widgets.ezt" False "tmpl" False ""]
+            [else]
+              <input disabled value = "
+              [for fields.values]
+                [fields.values.val]
+              [end]
+              " style="text-align:right; width:12em" class="multivalued customfield" aria-labelledby="[fields.field_id]_label">
+            [end]
+          [else]
+            [for fields.values][fields.values.val][end]
+          [end]
+        </td>
+      </tr>
+  [end][end]
+
+  [if-any allow_edit]
+    <tr>
+      <td></td>
+      <td>
+        <input id="submit_btn" type="submit" name="submit" value="Save template">
+        <input id="delete_btn" type="submit" class="secondary" name="deletetemplate" value="Delete Template">
+      </td>
+    </tr>
+  [end]
+
+</table>
+</form>
+
+[include "field-value-widgets-js.ezt"]
+
+[end][# end if not read_only]
+
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+
+  [if-any allow_edit]
+    let addPhasesCheckbox = document.getElementById('cb_add_approvals');
+    if (addPhasesCheckbox) {
+      addPhasesCheckbox.addEventListener('change', toggleGatesView);
+    }
+
+    var acobElements = document.getElementsByClassName("acob");
+    for (var i = 0; i < acobElements.length; ++i) {
+       var el = acobElements[[]i];
+       el.addEventListener("focus", function(event) {
+           _acrob(null);
+           _acof(event);
+       });
+    }
+
+    if ($("status")) {
+      _loadStatusSelect("[projectname]", "status", "[initial_status]");
+      $("status").addEventListener("focus", function(event) {
+        _acrob(null);
+      });
+    }
+
+    if($("res_fd_message")) {
+      $("res_fd_message").onclick = function(){
+        $("res_fd_banner").classList.add("hidden");
+      };
+    };
+
+  [else]
+
+    let labelInputs = document.getElementsByClassName("labelinput");
+    Array.prototype.forEach.call(labelInputs, labelInput => {
+      labelInput.disabled = true;
+    });
+  [end]
+
+  toggleGatesView();
+  function toggleGatesView() {
+    let addPhasesCheckbox = document.getElementById('cb_add_approvals');
+    if (addPhasesCheckbox === null) return;
+    let addPhases = addPhasesCheckbox.checked;
+    let subfieldRows = document.getElementsByClassName('subfield-row-class');
+    let phasefieldRows = document.getElementsByClassName('phasefield-row-class');
+    if (addPhases) {
+      $('launch-gates-table').classList.remove('hidden');
+      for (let i=0; i<subfieldRows.length; i++){
+        subfieldRows[[]i].classList.remove('hidden');
+      }
+      for (let i=0; i<phasefieldRows.length; i++){
+        phasefieldRows[[]i].classList.remove('hidden');
+      }
+    } else{
+      $('launch-gates-table').classList.add('hidden');
+      for (let i=0; i<subfieldRows.length; i++){
+        subfieldRows[[]i].classList.add('hidden');
+      }
+      for (let i=0; i<phasefieldRows.length; i++){
+        phasefieldRows[[]i].classList.add('hidden');
+      }
+    }
+  }
+
+});
+</script>
+
+[include "../framework/footer.ezt"]
diff --git a/templates/tracker/update-issues-hotlists-dialog.ezt b/templates/tracker/update-issues-hotlists-dialog.ezt
new file mode 100644
index 0000000..a453246
--- /dev/null
+++ b/templates/tracker/update-issues-hotlists-dialog.ezt
@@ -0,0 +1,11 @@
+[# TODO(jojwang): refine what buttons are shown when there are no hotlists]
+<div id="update-issues-hotlists" style="display: none">
+  <div id="update-issues-hotlists-dialog">
+    <table id="js-hotlists-table">
+    </table>
+    <menu>
+      <button id="cancel-update-hotlists" type="reset">Cancel</button>
+      <button id="save-issues-hotlists">Save</button>
+    </menu>
+  </div>
+</div>
diff --git a/templates/tracker/web-components-page.ezt b/templates/tracker/web-components-page.ezt
new file mode 100644
index 0000000..88a42e4
--- /dev/null
+++ b/templates/tracker/web-components-page.ezt
@@ -0,0 +1,41 @@
+[if-any local_id]
+  [define title][local_id][end]
+[else]
+  [define title]Monorail[end]
+[end]
+
+[define is_ezt][end]
+[include "../framework/header-shared.ezt"]
+
+[include "../webpack-out/mr-app.ezt"]
+
+<mr-app [if-any logged_in_user]
+  userDisplayName="[logged_in_user.email]"[end]
+  loginUrl="[login_url]"
+  logoutUrl="[logout_url]"
+  versionBase="[version_base]"
+></mr-app>
+
+[include "../framework/polymer-footer.ezt"]
+
+[if-any local_id]
+  <script type="text/javascript" nonce="[nonce]">
+    window.addEventListener('load', () => {
+      window.getTSMonClient().recordIssueDetailSpaTiming();
+    });
+  </script>
+[end]
+
+<script type="text/javascript" nonce="[nonce]">
+  runOnLoad(function() {
+    if (typeof(ClientLogger) === "function") {
+      let cl = new ClientLogger("issues");
+      if (cl.started("new-issue")) {
+        cl.logEnd("new-issue", null, 120 * 1000);
+      }
+      if (cl.started("issue-search")) {
+        cl.logEnd("issue-search");
+      }
+    }
+  });
+</script>