38 new commits in mtrack:
https://bitbucket.org/wez/mtrack/changeset/4ec6f098b02f/
changeset: 4ec6f098b02f
user: wez
date: 2012-04-24 06:06:15
summary: prototyping the latest incarnation of the ticket page.
This one hides the edit fields until you click the edit button
(everything old is new again...) but this incarnation uses a modal
dialog with dynamically constructed tabs and backbone models to achieve
its goals.
Still TODO: need renderers for the various field types in the
non-editable mode. Also need to plumb comments/changes and attachments.
Want to allow custom fields to specify an underscore compatible template
string to render the field contents. This is useful for example to
auto-link to resources in external systems (such as bugzilla)
affected #: 6 files
diff -r 230c7682b57252b004d1beb68e3999e62583a8c7 -r 4ec6f098b02fc830cbb684f77ca8814200345919 web/js/templates/item.changed.html
--- /dev/null
+++ b/web/js/templates/item.changed.html
@@ -0,0 +1,3 @@
+<img class='gravatar' src="<%= ABSWEB %>avatar.php?u=<%- who %>&s=24">
+<abbr class='timeinterval' title='<%- when %>'><%- when %></abbr> by
+ <a href="<%= ABSWEB %>user.php/<%- who %>"><%- who %></a>
diff -r 230c7682b57252b004d1beb68e3999e62583a8c7 -r 4ec6f098b02fc830cbb684f77ca8814200345919 web/js/templates/ticket.edit.html
--- /dev/null
+++ b/web/js/templates/ticket.edit.html
@@ -0,0 +1,71 @@
+<style>
+ div.ticketeditor table tr td {
+ vertical-align: middle;
+ padding-top: 0.5em;
+ }
+ div.ticketeditor table tr td.fieldvalue label {
+ font-weight: bold;
+ }
+ div.ticketeditor table tr td.fieldvalue input {
+ width: 25em;
+ font-size: 1.2em;
+ }
+ div.ticketeditor table tr td.fieldvalue ul.chzn-choices {
+ border: solid 1px #d7d7d7;
+ font-size: 1.2em;
+ margin: 2px;
+ }
+ div.ticketeditor table tr td.fieldvalue div.mf_container {
+ border: solid 1px #d7d7d7;
+ margin: 2px;
+ }
+ div.ticketeditor table tr td.fieldvalue div.mf_container,
+ div.ticketeditor table tr td.fieldvalue ol.mp_list {
+ font-size: 1.2em;
+ width: 26em;
+ }
+ div.ticketeditor table tr td.fieldname {
+ width: 10em;
+ font-weight: bold;
+ text-align: right;
+ }
+ .ticketeditor .modal-header {
+ border-bottom: none;
+ padding-bottom: 0px;
+ }
+ .ticketeditor .modal-body {
+ padding-top: 0px;
+ }
+</style>
+<div class='modal hide ticketeditor'>
+ <div class='modal-header'>
+ <a class='close' data-dismiss='modal'>×</a>
+ <% if (isnew) { %>
+ <h3>Create a new Ticket</h3>
+ <% } else { %>
+ <h3>Edit Ticket #<%- nsident %><%- summary %></h3>
+ <% } %>
+ <ul class="nav nav-tabs">
+ <li><a href='#' data-toggle='tab'></a></li>
+ </ul>
+
+ </div>
+ <div class='modal-body'>
+ <div class='tab-content'>
+ <div class='tab-pane'>
+ <table>
+ <tr>
+ <td class='fieldname'></td>
+ <td class='fieldvalue'></td>
+ </tr>
+ </table>
+ </div>
+ </div>
+ </div>
+ <div class='modal-footer'>
+ <button class='btn' data-dismiss='modal'>Cancel</button>
+ <button class='btn btn-primary'
+ ><% if (isnew) { %>Submit<% } else { %>Save<% } %> Ticket</button>
+ </div>
+</div>
+
diff -r 230c7682b57252b004d1beb68e3999e62583a8c7 -r 4ec6f098b02fc830cbb684f77ca8814200345919 web/js/templates/ticket.show.html
--- /dev/null
+++ b/web/js/templates/ticket.show.html
@@ -0,0 +1,71 @@
+<style>
+ section {
+ /* compensate for navbar offset for id=XXX anchor targets */
+ padding-top: 2.5em;
+ }
+ div#tkt-description {
+ border: solid 1px #ddd;
+ margin: -0.5em;
+ padding: 0.5em;
+ }
+ div.tkt-outline {
+ position: fixed;
+ right: 0.5em;
+ top: 3em;
+ width: 10em;
+ min-height: 10em;
+ border: solid 1px #ddd;
+ background-color: white;
+ }
+ #tkt-fields table tr td.fieldname {
+ text-align: right;
+ font-weight: bold;
+ color: #777;
+ }
+ #tkt-fields img.gravatar {
+ vertical-align: middle;
+ }
+</style>
+
+<div class='tkt-outline'>
+ <button class='btn btn-primary'><i class='icon-pencil icon-white'></i> Edit</button>
+ <ul id='tkt-nav' class='nav nav-pills'>
+ <li class='active'><a href='#fields'>Summary</a></li>
+ <li><a href='#description'>Description</a></li>
+ <li><a href='#attach'>Attachments</a></li>
+ <li><a href='#comments'>Comments</a></li>
+ </ul>
+</div>
+
+<section id='fields'>
+ <h1 class="ticket-summary <% if (status == 'closed') { %>closed<% } %>">
+ <% if (nsident) { %>
+ #<%- nsident %>
+ <% } else { %>
+ [NEW]
+ <% } %><%- summary %></h1>
+ <div id='tkt-fields' class='tkt-fields'>
+ <table>
+ <tr>
+ <td class='fieldname'></td>
+ <td class='fieldvalue'></td>
+ </tr>
+ </table>
+ </div>
+</section>
+
+<section id='description'>
+ <div class="wikipage" id="tkt-description"><%= description_html %></div>
+</section>
+
+<section id='attach'>
+ <div id='tkt-attach'>
+ <h2>Attachments</h2>
+ </div>
+</section>
+
+<section id='comments'>
+ <div id='tkt-comments'>
+ <h2>Comments</h2>
+ </div>
+</section>
diff -r 230c7682b57252b004d1beb68e3999e62583a8c7 -r 4ec6f098b02fc830cbb684f77ca8814200345919 web/js/templates/user.name.html
--- /dev/null
+++ b/web/js/templates/user.name.html
@@ -0,0 +1,2 @@
+<a href="<%= ABSWEB %>user.php/<%- id %>"
+ ><img class='gravatar' src="<%= ABSWEB %>avatar.php?u=<%- id %>&s=24"><%- label %></a>
diff -r 230c7682b57252b004d1beb68e3999e62583a8c7 -r 4ec6f098b02fc830cbb684f77ca8814200345919 web/js/views.js
--- a/web/js/views.js
+++ b/web/js/views.js
@@ -924,6 +924,9 @@
this.values = options.values;
this.readonly = options.readonly;
this.saveAfterEdit = options.saveAfterEdit;
+ if (!this.options.width) {
+ this.options.width = '220px';
+ }
if (!this.values) { // chosen doesn't like empty select elements
this.values = [{id:"", label: ""}];
}
@@ -1007,7 +1010,7 @@
add_items(sel, view.values);
- sel.css('width', '220px').chosen({
+ sel.css('width', view.options.width).chosen({
allow_single_deselect: true
}).change(function() {
var o = {};
diff -r 230c7682b57252b004d1beb68e3999e62583a8c7 -r 4ec6f098b02fc830cbb684f77ca8814200345919 web/ticket2.php
--- /dev/null
+++ b/web/ticket2.php
@@ -0,0 +1,508 @@
+<?php # vim:ts=2:sw=2:et:
+/* For licensing and copyright terms, see the file named LICENSE */
+include '../inc/common.php';
+
+if ($pi = mtrack_get_pathinfo()) {
+ $id = $pi;
+} else {
+ $id = $_GET['id'];
+}
+
+if ($id == 'new') {
+ $issue = new MTrackIssue;
+ $issue->priority = 'normal';
+} else {
+ if (strlen($id) == 32) {
+ $issue = MTrackIssue::loadById($id);
+ } else {
+ $issue = MTrackIssue::loadByNSIdent($id);
+ }
+ if (!$issue) {
+ throw new Exception("Invalid ticket $id");
+ }
+}
+
+$field_data = MTrackAPI::invoke('GET', '/ticket/meta/fields', null,
+ array('tid' => $issue->tid))->result;
+
+$FIELDSET = json_encode($field_data);
+
+if ($id == 'new') {
+ MTrackACL::requireAllRights("Tickets", 'create');
+ $editable = 'true';
+ mtrack_head("New ticket");
+ $TICKET = json_encode(MTrackIssue::rest_return_ticket($issue));
+ $CHANGES = json_encode(array());
+ $ATTACH = json_encode(array());
+} else {
+ MTrackACL::requireAllRights("ticket:" . $issue->tid, 'read');
+ $editable = json_encode(
+ MTrackACL::hasAllRights("ticket:" . $issue->tid, 'modify'));
+ if ($issue->nsident) {
+ mtrack_head("#$issue->nsident " . $issue->summary);
+ } else {
+ mtrack_head("#$id " . $issue->summary);
+ }
+ $TICKET = json_encode(MTrackAPI::invoke('GET', "/ticket/$id")->result);
+ $CHANGES = json_encode(MTrackAPI::invoke(
+ 'GET', "/ticket/$id/changes")->result);
+ $ATTACH = json_encode(MTrackAPI::invoke(
+ 'GET', "/ticket/$id/attach")->result);
+}
+
+echo <<<HTML
+<div id="ticket"></div>
+<script type='text/javascript'>
+// Ticket editor
+var TE = Backbone.View.extend({
+ show: function(on_success) {
+ var o = this.model.toJSON();
+ o.isnew = this.model.isNew();
+ // A clone of the model to use for editing with the existing
+ // set of editors
+ var dup_model = this.model.clone();
+
+ $(this.el).html(_.template(
+ mtrack_underscore_templates['ticket-edit'], o));
+
+ var view = this;
+ $(view.el).appendTo('body');
+ var dlg = $('div.modal', view.el);
+ var in_wiki = false;
+
+ dlg.on('hidden', function () {
+ if (!in_wiki) {
+ $(view.el).remove();
+ }
+ });
+
+ // Grab template elements and take them out of the DOM
+ var tab_ul = $('ul.nav-tabs', view.el);
+ var tab_hdr = $('li', tab_ul);
+ tab_hdr.remove();
+ var tab_content = $('div.tab-content', view.el);
+ var tab_body = $('div.tab-pane', tab_content);
+ tab_body.remove();
+
+ var next_tab_id = 1;
+ function add_group_tab(group) {
+ var label;
+ if (
group.name == 0) {
+ label = 'Details';
+ } else {
+ label =
group.name;
+ }
+
+ var id = 'tab-' + next_tab_id++;
+
+ var hdr = tab_hdr.clone();
+ $('a', hdr).attr('href', '#' + id);
+ $('a', hdr).text(label);
+ tab_ul.append(hdr);
+
+ var tab = tab_body.clone();
+ tab.attr('id', id);
+ tab_content.append(tab);
+
+ if (
group.name == 0) {
+ // Make the first one active
+ $('a', hdr).trigger('click');
+ }
+
+ console.log("made tab", group, tab);
+ return tab;
+ }
+
+ function multi_editor(lcont, tab, field) {
+ var label = $('<label/>');
+ label.text(field.label);
+ tab.append(label);
+
+ var edit = $('<textarea/>', {
+ cols: field.cols,
+ rows: field.rows,
+ placeholder: field.placeholder
+ });
+ var val = dup_model.get(
field.name);
+ if (val) {
+ edit.val(val);
+ }
+ edit.on('change', function() {
+ var o = {};
+ o[
field.name] = edit.val();
+ dup_model.set(o);
+ });
+ lcont.remove();
+ tab.append(edit);
+ tab.attr('colspan', 2);
+ }
+
+ function wiki_editor(lcont, tab, field) {
+ var label = $('<label/>');
+ label.text(field.label);
+ tab.append(label);
+
+ var edit = $('<textarea/>', {
+ class: 'wiki shortwiki',
+ cols: field.cols,
+ rows: field.rows,
+ placeholder: field.placeholder
+ });
+ var val = dup_model.get(
field.name);
+ if (val) {
+ edit.val(val);
+ }
+ lcont.remove();
+ tab.append(edit);
+ var b = $('<button/>', {
+ class: 'btn'
+ });
+ b.html('<i class="icon-pencil"></i> Edit ' + field.label + ' in wiki editor');
+ tab.append(b);
+ tab.attr('colspan', 2);
+
+ dup_model.bind('change:' +
field.name, function () {
+ var val = dup_model.get(
field.name);
+ edit.val(val ? val : '');
+ });
+ edit.on('change', function() {
+ var o = {};
+ o[
field.name] = edit.val();
+ dup_model.set(o);
+ });
+
+ var wiki = new MTrackWikiTextAreaView({
+ model: dup_model,
+ wikiContext: 'ticket:',
+ use_overlay: true,
+ Caption: "Edit " + field.label,
+ OKLabel: "Accept " + field.label,
+ CancelLabel: "Abandon changes to " + field.label,
+ srcattr:
field.name,
+ renderedattr:
field.name + '_html'
+ });
+ wiki.bind('editstart', function () {
+ in_wiki = true;
+ setTimeout(function () {
+ dlg.modal('hide');
+ }, 1000);
+ });
+ wiki.bind('editend', function () {
+ in_wiki = false;
+ dlg.modal('show');
+ });
+
+ b.click(function () {
+ var o = {};
+ o[
field.name] = edit.val();
+ dup_model.set(o);
+ wiki.edit();
+ });
+ }
+
+ function text_editor(lcont, tab, field) {
+ lcont.text(field.label);
+
+ var inp = $('<input/>', {
+ type: "text",
+ name:
field.name,
+ placeholder: field.placeholder
+ });
+ var val = view.model.get(
field.name);
+ if (val) {
+ inp.val(val);
+ }
+ inp.on('change', function() {
+ var o = {};
+ o[
field.name] = inp.val();
+ dup_model.set(o);
+ });
+
+ tab.append(inp);
+ }
+
+ function multiselect_editor(lcont, tab, field) {
+ lcont.text(field.label);
+
+ var el = $('<div/>');
+ tab.append(el);
+ var view = new MTrackSelectEditorView({
+ el: el,
+ model: dup_model,
+ multiple: true,
+ srcattr:
field.name,
+ label: field.label,
+ width: '418px',
+ values: field.options,
+ defval: field["default"],
+ placeholder: field.placeholder
+ });
+ view.render();
+ }
+
+ function select_editor(lcont, tab, field) {
+ lcont.text(field.label);
+
+ var el = $('<span/>');
+ tab.append(el);
+ var view = new MTrackSelectEditorView({
+ el: el,
+ model: dup_model,
+ srcattr:
field.name,
+ label: field.label,
+ width: '418px',
+ values: field.options,
+ defval: field["default"],
+ placeholder: field.placeholder
+ });
+ view.render();
+ }
+
+ function ticketdeps_editor(lcont, tab, field) {
+ lcont.text(field.label);
+
+ var el = $('<span/>');
+ tab.append(el);
+ var view = new MTrackTicketDepEditView({
+ el: el,
+ model: dup_model,
+ srcattr:
field.name,
+ label: field.label,
+ });
+ view.render();
+ }
+
+ function tags_editor(lcont, tab, field) {
+ lcont.text(field.label);
+
+ var el = $('<span/>');
+ tab.append(el);
+ var view = new MTrackTagEditView({
+ el: el,
+ model: dup_model,
+ srcattr:
field.name,
+ label: field.label,
+ });
+ view.render();
+ }
+
+ function cc_editor(lcont, tab, field) {
+ lcont.text(field.label);
+
+ var el = $('<span/>');
+ tab.append(el);
+ var view = new MTrackCcEditView({
+ el: el,
+ model: dup_model,
+ srcattr:
field.name,
+ label: field.label,
+ });
+ view.render();
+ }
+
+ var editors = {
+ multi: multi_editor,
+ select: select_editor,
+ multiselect: multiselect_editor,
+ tags: tags_editor,
+ cc: cc_editor,
+ ticketdeps: ticketdeps_editor,
+ text: text_editor,
+ wiki: wiki_editor
+ };
+
+ function add_editor(tr, tab, field)
+ {
+ if (field.type == 'readonly') return;
+
+ var editor = text_editor;
+
+ if (field.type in editors) {
+ editor = editors[field.type];
+ }
+ tr = tr.clone();
+ var lcont = $('td.fieldname', tr);
+ var div = $('td.fieldvalue', tr);
+ tab.append(tr);
+ editor(lcont, div, field);
+ }
+
+ /* Create a tab for each category of field */
+ for (var gidx in this.options.fields) {
+ var group = this.options.fields[gidx];
+ var tab = add_group_tab(group);
+
+ var table = $('table', tab);
+ var tr = $('tr', table);
+ tr.remove();
+
+ if (
group.name == 0) {
+ // Synthesize some fields
+ add_editor(tr, table, {
+ name: 'summary',
+ label: 'Summary'
+ });
+ add_editor(tr, table, {
+ name: 'status',
+ label: 'Status',
+ type: 'select',
+ options: mtrack_ticket_states
+ });
+ if (dup_model.get('status') != 'closed') {
+ add_editor(tr, table, {
+ name: 'resolution',
+ label: 'Resolution',
+ placeholder: 'Resolve ticket as...',
+ type: 'select',
+ options: mtrack_resolutions
+ });
+ }
+ }
+
+ _.each(group.fields, function (field) {
+ add_editor(tr, table, field);
+ });
+ }
+
+ // Save. We want to apply the attributes from the dup_model to
+ // the real model.
+ $('.modal-footer button.btn-primary', dlg).click(function () {
+ view.model.save(dup_model.attributes, {
+ success: function(model) {
+ on_success(model);
+ dlg.modal('hide');
+ },
+ error: function (model, resp) {
+ mtrack_ajax_error_to_dom(resp, $('div.modal-body', dlg));
+ }
+ });
+ });
+
+ dlg.modal('show');
+ }
+});
+
+// Ticket viewer
+var TV = Backbone.View.extend({
+ render: function() {
+ var t = _.template(mtrack_underscore_templates['ticket-show']);
+ var o = this.model.toJSON();
+ $(this.el).html(t(o));
+// $('body').attr('data-target', '#tkt-nav');
+// $('body').scrollspy({offset: 30});
+
+ var F = $('#tkt-fields');
+ // Table row template
+ var TR = F.find('tr');
+ var table = TR.parent();
+ TR.remove();
+
+ var model = this.model;
+
+ var user_template = _.template(mtrack_underscore_templates['user-name']);
+ function render_user(user) {
+ if (!_.isObject(user)) {
+ user = {
+ id: user,
+ label: user
+ };
+ }
+ var o = _.clone(user);
+ o.ABSWEB = ABSWEB;
+ return user_template(o);
+ }
+
+ function render_user_list(users) {
+ var res = [];
+ _.each(users, function(user) {
+ res.push(render_user(user));
+ });
+ return res.join(' ');
+ }
+
+ function render_change_time(item) {
+ item = _.clone(item);
+ item.ABSWEB = ABSWEB;
+ return _.template(mtrack_underscore_templates['item-changed'], item);
+ }
+
+ var renderers = {
+ 'owner': render_user,
+ 'cc': render_user_list,
+ 'created': render_change_time,
+ 'updated': render_change_time,
+ };
+
+ function process_field(field) {
+ if (
field.name == 'description') {
+ return;
+ }
+ var val = model.get(
field.name);
+ if (typeof(val) == 'undefined' || val == null) {
+ return;
+ }
+ if (_.isArray(val) && val.length == 0) {
+ return;
+ }
+ if (typeof(val) == 'string' && val == '') {
+ return;
+ }
+ console.log(field, val);
+ var tr = TR.clone();
+ $('td.fieldname', tr).text(field.label);
+ if (
field.name in renderers) {
+ $('td.fieldvalue', tr).html(renderers[
field.name](val));
+ } else {
+ $('td.fieldvalue', tr).text(val);
+ }
+ table.append(tr);
+ }
+
+ process_field({name: 'created', label: 'Opened'});
+ if (model.get('updated') &&
+ model.get('updated').cid != model.get('created').cid) {
+ process_field({name: 'updated', label: 'Updated'});
+ }
+
+ for (var gidx in this.options.fields) {
+ var group = this.options.fields[gidx];
+ console.log("group", group);
+ _.each(group.fields, process_field);
+ }
+ $('.timeinterval', this.el).timeago();
+
+ // Open the ticket editor
+ var view = this;
+ $('button.btn-primary', this.el).click(function () {
+ var editor = new TE({model: view.model, fields: view.options.fields});
+ editor.show(function (model) {
+ view.render();
+ });
+ });
+ return this;
+ }
+});
+
+$(document).ready(function() {
+ var TheTicket = null;
+ var base_ticket = $TICKET;
+ var FIELDSET = $FIELDSET;
+ var editable = $editable;
+ var editor = null;
+ var changes = $CHANGES;
+ var attachments = $ATTACH;
+ var comment_editor = null;
+
+ TheTicket = new MTrackTicket(base_ticket);
+
+ var V = new TV({
+ model: TheTicket,
+ fields: FIELDSET,
+ el: $('#ticket')
+ });
+ V.render();
+});
+</script>
+HTML;
+
+mtrack_foot();
https://bitbucket.org/wez/mtrack/changeset/bb15865f574d/
changeset: bb15865f574d
user: wez
date: 2012-04-24 23:00:48
summary: allow selecting the "spent" column for reports
affected #: 1 file
diff -r 2a76ab711b574771166b2806d19737401377469f -r bb15865f574d24a95fda995792586e52e58b1361 web/query.php
--- a/web/query.php
+++ b/web/query.php
@@ -66,7 +66,7 @@
}
$fields = array('cc', 'component', 'milestone',
- 'status', 'owner', 'closedmilestone', 'estimated', 'remaining',
+ 'status', 'owner', 'closedmilestone', 'estimated', 'remaining', 'spent',
'type', 'summary', 'ticket', 'priority', 'keyword', 'depends', 'blocks');
asort($fields);
https://bitbucket.org/wez/mtrack/changeset/8ddea9095ea5/
changeset: 8ddea9095ea5
user: wez
date: 2012-04-24 19:59:50
summary: fix for postgres compatibility
affected #: 1 file
diff -r 2a76ab711b574771166b2806d19737401377469f -r 8ddea9095ea540c2b7f4d036359aebf634026f98 inc/issue.php
--- a/inc/issue.php
+++ b/inc/issue.php
@@ -1855,9 +1855,11 @@
/* we only want to return matches that have no children of their own */
if (preg_match("/^[a-z0-9A-Z]+$/", $id)) {
- foreach (MTrackDB::q("select tid, nsident, summary, (select count(k.tid) from tickets k where k.ptid = t.tid) as kids from tickets t where nsident like '$id%' and kids = 0 and status != 'closed'")->fetchAll(PDO::FETCH_OBJ)
+ foreach (MTrackDB::q("select tid, nsident, summary, (select count(k.tid) from tickets k where k.ptid = t.tid) as kids from tickets t where nsident like '$id%' and status != 'closed'")->fetchAll(PDO::FETCH_OBJ)
as $obj) {
- $res[$obj->tid] = $obj;
+ if ($obj->kids == 0) {
+ $res[$obj->tid] = $obj;
+ }
}
}
if (!preg_match("/\d/", $q)) {
https://bitbucket.org/wez/mtrack/changeset/ed5c2a5c8e51/
changeset: ed5c2a5c8e51
user: wez
date: 2012-04-24 23:01:35
summary: if not globally privileged to create repos, and allow user repo creation
is set, allow user to create their own repos.
affected #: 1 file
diff -r bb15865f574d24a95fda995792586e52e58b1361 -r ed5c2a5c8e51ff5087da8ac7bb7c4122e849bf32 inc/scm.php
--- a/inc/scm.php
+++ b/inc/scm.php
@@ -941,6 +941,9 @@
$res['project:' . $row[1]] = $row[1];
}
}
+ } else if (MTrackConfig::get('repos', 'allow_user_repo_creation')) {
+ $me = mtrack_canon_username(MTrackAuth::whoami());
+ $res = array("user:$me" => $me);
}
return $res;
https://bitbucket.org/wez/mtrack/changeset/4fa48255de4b/
changeset: 4fa48255de4b
user: wez
date: 2012-04-24 23:01:55
summary: merge postgres fix
affected #: 1 file
diff -r ed5c2a5c8e51ff5087da8ac7bb7c4122e849bf32 -r 4fa48255de4b51ffcf5bf09cdd60b8381475caf0 inc/issue.php
--- a/inc/issue.php
+++ b/inc/issue.php
@@ -1855,9 +1855,11 @@
/* we only want to return matches that have no children of their own */
if (preg_match("/^[a-z0-9A-Z]+$/", $id)) {
- foreach (MTrackDB::q("select tid, nsident, summary, (select count(k.tid) from tickets k where k.ptid = t.tid) as kids from tickets t where nsident like '$id%' and kids = 0 and status != 'closed'")->fetchAll(PDO::FETCH_OBJ)
+ foreach (MTrackDB::q("select tid, nsident, summary, (select count(k.tid) from tickets k where k.ptid = t.tid) as kids from tickets t where nsident like '$id%' and status != 'closed'")->fetchAll(PDO::FETCH_OBJ)
as $obj) {
- $res[$obj->tid] = $obj;
+ if ($obj->kids == 0) {
+ $res[$obj->tid] = $obj;
+ }
}
}
if (!preg_match("/\d/", $q)) {
https://bitbucket.org/wez/mtrack/changeset/c8c297b66ceb/
changeset: c8c297b66ceb
user: wez
date: 2012-04-25 02:21:52
summary: merge heads
affected #: 5 files
diff -r 4ec6f098b02fc830cbb684f77ca8814200345919 -r c8c297b66cebffb59f9c78890ba46c4249af1b01 inc/database.php
--- a/inc/database.php
+++ b/inc/database.php
@@ -427,6 +427,8 @@
$db->sqliteCreateFunction('mtrack_cleanup_attachments',
array('MTrackAttachment', 'attachment_row_deleted'));
+
+ $db->sqliteCreateFunction('greatest', 'max');
}
foreach (self::$extensions as $ext) {
diff -r 4ec6f098b02fc830cbb684f77ca8814200345919 -r c8c297b66cebffb59f9c78890ba46c4249af1b01 inc/issue.php
--- a/inc/issue.php
+++ b/inc/issue.php
@@ -1712,20 +1712,25 @@
}
}
+ // This is much more complex than I'd like it to be :-/
+ // This logic MUST be equivalent to that of the 'remaining' field
+ // in inc/report.php.
function getRemaining() {
if ($this->status == 'closed') {
return 0;
}
foreach (MTrackDB::q(<<<SQL
-SELECT coalesce(
- case when sum(remaining) < 0 then 0 else
- round(cast(sum(remaining) as numeric), 2)
- end, ?)
- from effort where tid = ?
+SELECT
+ count(remaining),
+ round(cast(? + coalesce(sum(remaining), 0) as numeric), 2)
+FROM effort where tid = ? and remaining != 0
SQL
, $this->estimated, $this->tid
)->fetchAll() as $row) {
- return $row[0] == 0 ? 0 : $row[0];
+ if ($row[0]) {
+ /* normalize floating point zero to precisely zero */
+ return $row[1] == 0 ? 0 : max($row[1], 0);
+ }
}
return $this->estimated;
}
@@ -1850,9 +1855,11 @@
/* we only want to return matches that have no children of their own */
if (preg_match("/^[a-z0-9A-Z]+$/", $id)) {
- foreach (MTrackDB::q("select tid, nsident, summary, (select count(k.tid) from tickets k where k.ptid = t.tid) as kids from tickets t where nsident like '$id%' and kids = 0 and status != 'closed'")->fetchAll(PDO::FETCH_OBJ)
+ foreach (MTrackDB::q("select tid, nsident, summary, (select count(k.tid) from tickets k where k.ptid = t.tid) as kids from tickets t where nsident like '$id%' and status != 'closed'")->fetchAll(PDO::FETCH_OBJ)
as $obj) {
- $res[$obj->tid] = $obj;
+ if ($obj->kids == 0) {
+ $res[$obj->tid] = $obj;
+ }
}
}
if (!preg_match("/\d/", $q)) {
diff -r 4ec6f098b02fc830cbb684f77ca8814200345919 -r c8c297b66cebffb59f9c78890ba46c4249af1b01 inc/report.php
--- a/inc/report.php
+++ b/inc/report.php
@@ -569,7 +569,33 @@
tk left join keywords k on (tk.kid = k.kid)
where tk.tid = t.tid) as keyword',
'type' => 'classification as type',
- 'remaining' => "(case when t.status = 'closed' then 0 else (select coalesce(case when sum(remaining) < 0 then 0 else round(cast(sum(remaining) as numeric), 2) end, t.estimated) from effort where effort.tid = t.tid) end) as remaining",
+ 'remaining' =>
+ // This is much more complex than I'd like it to be :-/
+ // This logic MUST be equivalent to that of MTrackIssue::getRemaining
+ // Logic is: if we have any non-zero effort entries, we sum them to
+ // get the remaining time, otherwise we use the estimated value.
+ // Except when the ticket is closed: show 0 then.
+ <<<SQL
+(
+ case when
+ t.status = 'closed' then
+ 0
+ else (
+ select
+ greatest(
+ round(
+ cast(t.estimated as numeric) +
+ cast(coalesce(sum(remaining), 0) as numeric
+ ), 2),
+ 0
+ )
+ from effort where effort.tid = t.tid and remaining != 0
+ )
+ end
+
+) as remaining
+SQL
+ ,
'state' => "(case when t.status = 'closed' then coalesce(t.resolution, 'closed') else t.status end) as state",
'milestone' => '(select mtrack_group_concat(name) from ticket_milestones
tmm left join milestones tmmm on (tmm.mid = tmmm.mid)
diff -r 4ec6f098b02fc830cbb684f77ca8814200345919 -r c8c297b66cebffb59f9c78890ba46c4249af1b01 inc/scm.php
--- a/inc/scm.php
+++ b/inc/scm.php
@@ -941,6 +941,9 @@
$res['project:' . $row[1]] = $row[1];
}
}
+ } else if (MTrackConfig::get('repos', 'allow_user_repo_creation')) {
+ $me = mtrack_canon_username(MTrackAuth::whoami());
+ $res = array("user:$me" => $me);
}
return $res;
diff -r 4ec6f098b02fc830cbb684f77ca8814200345919 -r c8c297b66cebffb59f9c78890ba46c4249af1b01 web/query.php
--- a/web/query.php
+++ b/web/query.php
@@ -66,7 +66,7 @@
}
$fields = array('cc', 'component', 'milestone',
- 'status', 'owner', 'closedmilestone', 'estimated', 'remaining',
+ 'status', 'owner', 'closedmilestone', 'estimated', 'remaining', 'spent',
'type', 'summary', 'ticket', 'priority', 'keyword', 'depends', 'blocks');
asort($fields);
https://bitbucket.org/wez/mtrack/changeset/dd777be24ee9/
changeset: dd777be24ee9
user: wez
date: 2012-04-25 03:07:15
summary: add comment field to editor. add generic field formatter for viewer.
affected #: 3 files
diff -r c8c297b66cebffb59f9c78890ba46c4249af1b01 -r dd777be24ee9297ae88eccfdb5d0b4eeb2ddfcc1 web/css/links.css
--- a/web/css/links.css
+++ b/web/css/links.css
@@ -9,6 +9,7 @@
a.changesetlink, a.wikilink,
span.branchname,
span.milestone, span.milestone a,
+a.milestone,
a.keyword, span.keyword a,
a.ticketlink, span.ticketlink a,
span.tagname
@@ -72,16 +73,19 @@
/* milestones and tags: blue */
span.tagname,
+a.milestone,
span.milestone {
background-color: #bef;
border: solid 1px #62CFFC;
color: #444;
}
+a.milestone:hover,
span.milestone:hover {
background-color: #62CFFC;
border: solid 1px #62CFFC;
color: #444;
}
+a.milestone:hover,
span.milestone a:hover {
text-decoration: none;
}
@@ -124,6 +128,7 @@
a.changesetlink, a.wikilink,
span.branchname,
span.milestone, span.milestone a,
+ a.milestone,
a.keyword, span.keyword a,
a.ticketlink, span.ticketlink a,
span.tagname {
diff -r c8c297b66cebffb59f9c78890ba46c4249af1b01 -r dd777be24ee9297ae88eccfdb5d0b4eeb2ddfcc1 web/js/templates/ticket.field.byname.milestones.html
--- /dev/null
+++ b/web/js/templates/ticket.field.byname.milestones.html
@@ -0,0 +1,1 @@
+<a class='milestone' href="<%= ABSWEB %>milestone.php/<%- id %>"><%- label %></a>
diff -r c8c297b66cebffb59f9c78890ba46c4249af1b01 -r dd777be24ee9297ae88eccfdb5d0b4eeb2ddfcc1 web/ticket2.php
--- a/web/ticket2.php
+++ b/web/ticket2.php
@@ -104,12 +104,11 @@
tab.attr('id', id);
tab_content.append(tab);
- if (
group.name == 0) {
+ if (next_tab_id == 2) {
// Make the first one active
$('a', hdr).trigger('click');
}
- console.log("made tab", group, tab);
return tab;
}
@@ -327,9 +326,7 @@
editor(lcont, div, field);
}
- /* Create a tab for each category of field */
- for (var gidx in this.options.fields) {
- var group = this.options.fields[gidx];
+ function add_tab_and_fields(group) {
var tab = add_group_tab(group);
var table = $('table', tab);
@@ -358,12 +355,30 @@
});
}
}
-
_.each(group.fields, function (field) {
add_editor(tr, table, field);
});
}
+ var com_tab = add_tab_and_fields({
+ name: 'Comment',
+ fields: [
+ {
+ name: 'comment',
+ label: 'Comment',
+ type: 'wiki',
+ placeholder: 'Something on your mind? Share it here!',
+ rows: 10,
+ cols: 78
+ }
+ ]
+ });
+
+ /* Create a tab for each category of field */
+ for (var gidx in this.options.fields) {
+ add_tab_and_fields(this.options.fields[gidx]);
+ }
+
// Save. We want to apply the attributes from the dup_model to
// the real model.
$('.modal-footer button.btn-primary', dlg).click(function () {
@@ -447,13 +462,55 @@
if (typeof(val) == 'string' && val == '') {
return;
}
- console.log(field, val);
var tr = TR.clone();
$('td.fieldname', tr).text(field.label);
if (
field.name in renderers) {
$('td.fieldvalue', tr).html(renderers[
field.name](val));
} else {
- $('td.fieldvalue', tr).text(val);
+ var out = [];
+
+ // If we have a template defined, then use the generic template
+ // based renderer. First look to see if we have a template
+ // for the specific field name, then try to fall back to a
+ // generic formatter by type.
+ var tplname = "ticket-field-byname-" +
field.name;
+ if (!(tplname in mtrack_underscore_templates)) {
+ // Try by type
+ tplname = "ticket-field-bytype-" + field.type;
+ }
+
+ var render = function (a) {
+ return $('<div/>').text(a).html();
+ };
+
+ if (tplname in mtrack_underscore_templates) {
+ var t = _.template(mtrack_underscore_templates[tplname]);
+ render = function (a) {
+ var o;
+ if (!_.isObject(a)) {
+ o = {
+ id: a,
+ label: a
+ };
+ } else {
+ o = _.clone(a);
+ }
+ o.ABSWEB = ABSWEB;
+ return t(o);
+ };
+ }
+ if (_.isObject(val)) {
+ _.each(val, function (v) {
+ if (_.isObject(v)) {
+ out.push(render(v.label));
+ } else {
+ out.push(render(v));
+ }
+ });
+ } else {
+ out.push(render(val));
+ }
+ $('td.fieldvalue', tr).html(out.join(' '));
}
table.append(tr);
}
@@ -466,7 +523,6 @@
for (var gidx in this.options.fields) {
var group = this.options.fields[gidx];
- console.log("group", group);
_.each(group.fields, process_field);
}
$('.timeinterval', this.el).timeago();
https://bitbucket.org/wez/mtrack/changeset/68f3b5847344/
changeset: 68f3b5847344
user: wez
date: 2012-04-25 04:15:32
summary: slightly more rational approach; less ambiguous and magical
affected #: 2 files
diff -r dd777be24ee9297ae88eccfdb5d0b4eeb2ddfcc1 -r 68f3b58473442e2a77ceb86ec185bb014bd3e48a web/js/templates/ticket.field.byname.milestones.html
--- a/web/js/templates/ticket.field.byname.milestones.html
+++ b/web/js/templates/ticket.field.byname.milestones.html
@@ -1,1 +1,4 @@
-<a class='milestone' href="<%= ABSWEB %>milestone.php/<%- id %>"><%- label %></a>
+<% _.each(value, function (label, id) { %>
+<a class='milestone'
+ href="<%= ABSWEB %>milestone.php/<%- label %>"><%- label %></a>
+<% }); %>
diff -r dd777be24ee9297ae88eccfdb5d0b4eeb2ddfcc1 -r 68f3b58473442e2a77ceb86ec185bb014bd3e48a web/ticket2.php
--- a/web/ticket2.php
+++ b/web/ticket2.php
@@ -486,30 +486,18 @@
if (tplname in mtrack_underscore_templates) {
var t = _.template(mtrack_underscore_templates[tplname]);
render = function (a) {
- var o;
- if (!_.isObject(a)) {
- o = {
- id: a,
- label: a
- };
- } else {
- o = _.clone(a);
- }
- o.ABSWEB = ABSWEB;
+ /* wrap it up so that the value itself is accessible,
+ * as some of the data types we have encode the ids
+ * in the keys of an object and we can't iterate
+ * the context without a name in the template handler */
+ var o = {
+ value: a,
+ ABSWEB: ABSWEB
+ };
return t(o);
};
}
- if (_.isObject(val)) {
- _.each(val, function (v) {
- if (_.isObject(v)) {
- out.push(render(v.label));
- } else {
- out.push(render(v));
- }
- });
- } else {
- out.push(render(val));
- }
+ out.push(render(val));
$('td.fieldvalue', tr).html(out.join(' '));
}
table.append(tr);
https://bitbucket.org/wez/mtrack/changeset/7018943e5504/
changeset: 7018943e5504
user: wez
date: 2012-04-25 04:16:35
summary: no need for the array now either
affected #: 1 file
diff -r 68f3b58473442e2a77ceb86ec185bb014bd3e48a -r 7018943e5504a42798cf12286325d178c16739b7 web/ticket2.php
--- a/web/ticket2.php
+++ b/web/ticket2.php
@@ -467,8 +467,6 @@
if (
field.name in renderers) {
$('td.fieldvalue', tr).html(renderers[
field.name](val));
} else {
- var out = [];
-
// If we have a template defined, then use the generic template
// based renderer. First look to see if we have a template
// for the specific field name, then try to fall back to a
@@ -497,8 +495,7 @@
return t(o);
};
}
- out.push(render(val));
- $('td.fieldvalue', tr).html(out.join(' '));
+ $('td.fieldvalue', tr).html(render(val));
}
table.append(tr);
}
https://bitbucket.org/wez/mtrack/changeset/76604b5e3891/
changeset: 76604b5e3891
user: wez
date: 2012-04-25 05:15:59
summary: add conflict resolver dialog
affected #: 3 files
diff -r 7018943e5504a42798cf12286325d178c16739b7 -r 76604b5e3891ad5b6329aa2175c82ece19fc2917 web/js/templates/ticket.conflict.html
--- /dev/null
+++ b/web/js/templates/ticket.conflict.html
@@ -0,0 +1,66 @@
+<div class='modal hide ticketconflict'>
+ <style>
+ div.ticketconflict table {
+ margin-top: 1em;
+ border: solid 1px #ddd;
+ border-collapse: collapse;
+ }
+ div.ticketconflict table thead tr td {
+ text-align: center;
+ font-weight: bold;
+ }
+ div.ticketconflict table tr td {
+ vertical-align: middle;
+ border: solid 1px #ddd;
+ padding: 0.5em;
+ }
+ div.ticketconflict table tr td.fieldname {
+ color: #777;
+ }
+ div.ticketconflict table tr td.fieldvalue {
+ width: 50%;
+ text-align: center;
+ }
+
+ </style>
+ <div class='modal-header'>
+ <a class='close' data-dismiss='modal'>×</a>
+ <h3>Resolve Conflicting Changes: #<%- nsident %><%- summary %></h3>
+ </div>
+ <div class='modal-body'>
+ <p>
+ A conflicting change was made:
+ </p>
+ <p>
+ <img class='gravatar' height='32' width='32'
+ src="<%= ABSWEB %>avatar.php?u=<%- updated.who %>&s=32">
+ <abbr class='timeinterval' title='<%- updated.when %>'
+ ><%- updated.when %></abbr> by
+ <a href="<%= ABSWEB %>user.php/<%- updated.who %>"><%- updated.who %></a>
+ </p>
+ <table>
+ <thead>
+ <tr>
+ <td>Field</td>
+ <td>Mine</td>
+ <td>Theirs</td>
+ </tr>
+ </thead>
+ <tbody>
+ <% _.each(conflict, function (item, field) {
+ %>
+ <tr>
+ <td class='fieldname'><%- field %></td>
+ <td class='fieldvalue mine'><%- item[0] %></td>
+ <td class='fieldvalue theirs'><%- item[1] %></td>
+ </tr>
+ <% }); %>
+ </tbody>
+ </table>
+ </div>
+ <div class='modal-footer'>
+ <button class='btn' data-dismiss='modal'>Cancel (lose all edits)</button>
+ <button class='btn mine'>Keep my changes</button>
+ <button class='btn theirs'>Take their changes</button>
+ </div>
+</div>
diff -r 7018943e5504a42798cf12286325d178c16739b7 -r 76604b5e3891ad5b6329aa2175c82ece19fc2917 web/js/templates/ticket.edit.html
--- a/web/js/templates/ticket.edit.html
+++ b/web/js/templates/ticket.edit.html
@@ -63,6 +63,7 @@
</div></div><div class='modal-footer'>
+ <!-- button class='btn conflict'>Conflict</button --><button class='btn' data-dismiss='modal'>Cancel</button><button class='btn btn-primary'
><% if (isnew) { %>Submit<% } else { %>Save<% } %> Ticket</button>
diff -r 7018943e5504a42798cf12286325d178c16739b7 -r 76604b5e3891ad5b6329aa2175c82ece19fc2917 web/ticket2.php
--- a/web/ticket2.php
+++ b/web/ticket2.php
@@ -379,6 +379,79 @@
add_tab_and_fields(this.options.fields[gidx]);
}
+ // Present the conflict resolution UI.
+ // This is an alternative form that shows the changes side-by-side
+ // and allows the user to pick a resolution:
+ // - Accept my changes
+ // - Accept their changes
+ // - Cancel
+ // The first two will re-display the edit dialog with the updated
+ // model, the latter will cancel the edit dialog.
+ function show_conflict_resolver(conflict) {
+ var updated = conflict.updated;
+ delete conflict.updated;
+ delete conflict.description_html;
+ var o = {
+ nsident: dup_model.get('nsident'),
+ summary: dup_model.get('summary'),
+ conflict: conflict,
+ updated: updated,
+ ABSWEB: ABSWEB
+ };
+ var CD = $(_.template(mtrack_underscore_templates['ticket-conflict'], o));
+ $('body').append(CD);
+ $('.timeinterval', CD).timeago();
+
+ // Fixup model so that we don't trigger a 409 on next save
+ // (unless there is a further conflict!)
+ dup_model.set({updated: o.updated});
+
+ dlg.modal('hide');
+ CD.modal('show');
+ CD.on('hidden', function() {
+ CD.remove();
+ });
+
+ $('button.mine', CD).click(function () {
+ var editor = new TE({
+ model: dup_model,
+ fields: view.options.fields
+ });
+ console.log("mine");
+ CD.modal('hide');
+ editor.show(on_success);
+ });
+
+ $('button.theirs', CD).click(function () {
+ var o = {};
+ for (var k in conflict) {
+ var item = conflict[k];
+ o[k] = item[1];
+ }
+ console.log("theirs", o);
+ dup_model.set(o);
+
+ var editor = new TE({
+ model: dup_model,
+ fields: view.options.fields
+ });
+ CD.modal('hide');
+ editor.show(on_success);
+ });
+ }
+ /*
+ $('.modal-footer button.conflict', dlg).click(function () {
+ show_conflict_resolver({
+ updated: {
+ who: 'otherguy',
+ when: "2012-04-11T14:54:36+00:00",
+ cid: "17"
+ },
+ summary: ['my lemons', 'your lemons']
+ });
+ });
+ */
+
// Save. We want to apply the attributes from the dup_model to
// the real model.
$('.modal-footer button.btn-primary', dlg).click(function () {
@@ -388,7 +461,26 @@
dlg.modal('hide');
},
error: function (model, resp) {
- mtrack_ajax_error_to_dom(resp, $('div.modal-body', dlg));
+ // If a conflict was detected, show some useful UI to help
+ // them through it
+ var is_conflict = false;
+
+ if (_.isObject(resp)) {
+ try {
+ var r = JSON.parse(resp.responseText);
+ if (r.code == 409) {
+ is_conflict = r.extra;
+ }
+ } catch (e) {
+ }
+ }
+
+ if (!is_conflict) {
+ mtrack_ajax_error_to_dom(resp, $('div.modal-body', dlg));
+ return;
+ }
+
+ show_conflict_resolver(is_conflict);
}
});
});
https://bitbucket.org/wez/mtrack/changeset/63905bb81a50/
changeset: 63905bb81a50
user: wez
date: 2012-04-25 06:10:14
summary: add attachment uploading
affected #: 3 files
diff -r 76604b5e3891ad5b6329aa2175c82ece19fc2917 -r 63905bb81a502d45840c49fe7b54ac2ed39c0313 web/js/templates/ticket.edit.html
--- a/web/js/templates/ticket.edit.html
+++ b/web/js/templates/ticket.edit.html
@@ -47,6 +47,8 @@
<% } %><ul class="nav nav-tabs"><li><a href='#' data-toggle='tab'></a></li>
+ <li><a href='#tkt-edit-attachments'
+ data-toggle='tab'>Attachments</a></li></ul></div>
@@ -60,6 +62,20 @@
</tr></table></div>
+ <div class='tab-pane' id='tkt-edit-attachments'>
+ <form action="<%= ABSWEB %>post-attachment.php" method="POST"
+ id="upload-form" enctype="multipart/form-data" target="upload_target">
+ <input type="hidden" name="object" value="ticket:<%- id %>">
+ <label for='attachments[]'>Select file(s) to be attached</label>
+ <input name="attachments[]" class='btn multi' type="file">
+ <iframe id="upload_target" name="upload_target"
+ src="<%= ABSWEB %>/post-attachment.php">
+ </iframe>
+ <br>
+ <input type="submit" class='btn'
+ id="confirm-upload" value="Upload">
+ </form>
+ </div></div></div><div class='modal-footer'>
diff -r 76604b5e3891ad5b6329aa2175c82ece19fc2917 -r 63905bb81a502d45840c49fe7b54ac2ed39c0313 web/post-attachment.php
--- a/web/post-attachment.php
+++ b/web/post-attachment.php
@@ -7,6 +7,15 @@
header('Content-Disposition: inline');
ob_start();
+if ($_SERVER['REQUEST_METHOD'] == 'GET') {
+ $res = new stdclass;
+ $res->status = 'success';
+ $res->code = 0;
+ $res->message = 'nothing to do';
+ echo json_encode($res);
+ exit;
+}
+
$object = $_POST['object'];
try {
diff -r 76604b5e3891ad5b6329aa2175c82ece19fc2917 -r 63905bb81a502d45840c49fe7b54ac2ed39c0313 web/ticket2.php
--- a/web/ticket2.php
+++ b/web/ticket2.php
@@ -78,10 +78,12 @@
// Grab template elements and take them out of the DOM
var tab_ul = $('ul.nav-tabs', view.el);
- var tab_hdr = $('li', tab_ul);
+ var tab_hdr = $('li:first', tab_ul);
tab_hdr.remove();
+ var tab_att = $('li', tab_ul);
+ tab_att.remove(); // we'll append it at the end
var tab_content = $('div.tab-content', view.el);
- var tab_body = $('div.tab-pane', tab_content);
+ var tab_body = $('div.tab-pane:first', tab_content);
tab_body.remove();
var next_tab_id = 1;
@@ -378,6 +380,37 @@
for (var gidx in this.options.fields) {
add_tab_and_fields(this.options.fields[gidx]);
}
+ tab_ul.append(tab_att);
+
+ /* handle uploads */
+ var uploading = false;
+ $('#confirm-upload', dlg).click(function () {
+ uploading = true;
+ $('#upload-form', dlg).submit();
+ });
+ $('#upload_target', dlg).on('load', function () {
+ console.log("upload target loaded");
+ var res = $(this).contents().find('body').text();
+ try {
+ res = JSON.parse(res);
+ if (res.status == 'success') {
+ if (uploading) {
+ $('<div class="alert alert-success">' +
+ '<a class="close" data-dismiss="alert">×</a>' +
+ 'Upload successful</div>').
+ appendTo($('#tkt-edit-attachments', dlg));
+ }
+ $('input[type=file]', dlg).val('');
+ } else {
+ $('<div class="alert alert-danger">' +
+ '<a class="close" data-dismiss="alert">×</a>' +
+ res.message + '</div>').
+ appendTo($('#tkt-edit-attachments', dlg));
+ }
+ } catch (e) {
+ }
+ uploading = false;
+ });
// Present the conflict resolution UI.
// This is an alternative form that shows the changes side-by-side
https://bitbucket.org/wez/mtrack/changeset/e5f36d717491/
changeset: e5f36d717491
user: wez
date: 2012-04-25 06:50:07
summary: allow deleting attachments
affected #: 3 files
diff -r 63905bb81a502d45840c49fe7b54ac2ed39c0313 -r e5f36d717491ac8629b90152d61bbee0ea1bc461 web/js/templates/attachment.item.html
--- /dev/null
+++ b/web/js/templates/attachment.item.html
@@ -0,0 +1,17 @@
+<div class='attachment'>
+ <a class='attachment' href='<%= ABSWEB %>attachment.php/<%- object %>/<%- cid %>/<%- filename %>'><%- filename %></a> (<%- size %>) added by <%- who %>
+ <abbr class='timeinterval' title='<%- changedate %>'><%- changedate %></abbr>
+ <button class='btn btn-mini delattach'><i class='icon-trash'></i></button>
+ <% if (image) {
+ var w = parseInt(width);
+ var h = parseInt(height);
+ var mw = 500;
+ if (w > mw) {
+ var s = w / mw;
+ height = h / s;
+ width = mw;
+ }
+ %>
+ <br><a href='<%= ABSWEB %>attachment.php/<%- object %>/<%- cid %>/<%- filename %>'><img src='<%= ABSWEB %>attachment.php/<%- object %>/<%- cid %>/<%- filename %>' width='<%- width %>' height='<%- height %>' border='0'></a>
+ <% } %>
+</div>
diff -r 63905bb81a502d45840c49fe7b54ac2ed39c0313 -r e5f36d717491ac8629b90152d61bbee0ea1bc461 web/js/templates/ticket.edit.html
--- a/web/js/templates/ticket.edit.html
+++ b/web/js/templates/ticket.edit.html
@@ -36,6 +36,11 @@
.ticketeditor .modal-body {
padding-top: 0px;
}
+ .ticketeditor #attach-list {
+ padding-bottom: 1em;
+ margin-bottom: 1em;
+ border-bottom: solid 1px #ddd;
+ }
</style><div class='modal hide ticketeditor'><div class='modal-header'>
@@ -63,6 +68,8 @@
</table></div><div class='tab-pane' id='tkt-edit-attachments'>
+ <div id='attach-list'></div>
+
<form action="<%= ABSWEB %>post-attachment.php" method="POST"
id="upload-form" enctype="multipart/form-data" target="upload_target"><input type="hidden" name="object" value="ticket:<%- id %>">
diff -r 63905bb81a502d45840c49fe7b54ac2ed39c0313 -r e5f36d717491ac8629b90152d61bbee0ea1bc461 web/ticket2.php
--- a/web/ticket2.php
+++ b/web/ticket2.php
@@ -61,6 +61,7 @@
// A clone of the model to use for editing with the existing
// set of editors
var dup_model = this.model.clone();
+ dup_model.getAttachments().reset(this.model.getAttachments().models);
$(this.el).html(_.template(
mtrack_underscore_templates['ticket-edit'], o));
@@ -76,6 +77,45 @@
}
});
+ var attach_list = $('#attach-list', dlg);
+ attach_list.on('click', 'button.delattach', function() {
+ var att = $(this).closest('div.attachment').data('attachment-model');
+ var m = $("<div class='modal fade'><div class='modal-header'><a class='close' data-dismiss='modal'>x</a><h3>Delete Attachment?</h3></div><div class='modal-body'><p><b></b></p><p>Do you really want to delete this attachment?</p><p>You cannot undo this action!</p></div><div class='modal-footer'><button class='btn' data-dismiss='modal'>Close</button><button class='btn btn-danger'>Delete</button></div></div>");
+
+ $('b', m).text(att.get('filename'));
+ $('.btn-danger', m).click(function () {
+ att.destroy();
+ m.modal('hide');
+ });
+
+ m.on('hidden', function() {
+ m.remove();
+ });
+ m.modal();
+
+ });
+ function redraw_attachment_list() {
+ attach_list.empty();
+ var t = _.template(mtrack_underscore_templates['attachment-item']);
+
+ dup_model.getAttachments().each(function (att) {
+ var o = att.toJSON();
+ if ('width' in o) {
+ o.image = true;
+ } else {
+ o.image = false;
+ }
+ var d = $(t(o));
+ d.data('attachment-model', att);
+ attach_list.append(d);
+ });
+ $('.timeinterval', attach_list).timeago();
+ }
+ redraw_attachment_list();
+ dup_model.getAttachments().bind('all', function () {
+ redraw_attachment_list();
+ });
+
// Grab template elements and take them out of the DOM
var tab_ul = $('ul.nav-tabs', view.el);
var tab_hdr = $('li:first', tab_ul);
@@ -389,7 +429,6 @@
$('#upload-form', dlg).submit();
});
$('#upload_target', dlg).on('load', function () {
- console.log("upload target loaded");
var res = $(this).contents().find('body').text();
try {
res = JSON.parse(res);
@@ -399,6 +438,7 @@
'<a class="close" data-dismiss="alert">×</a>' +
'Upload successful</div>').
appendTo($('#tkt-edit-attachments', dlg));
+ dup_model.getAttachments().reset(res.attachments);
}
$('input[type=file]', dlg).val('');
} else {
@@ -450,7 +490,6 @@
model: dup_model,
fields: view.options.fields
});
- console.log("mine");
CD.modal('hide');
editor.show(on_success);
});
@@ -461,7 +500,6 @@
var item = conflict[k];
o[k] = item[1];
}
- console.log("theirs", o);
dup_model.set(o);
var editor = new TE({
@@ -640,7 +678,10 @@
// Open the ticket editor
var view = this;
$('button.btn-primary', this.el).click(function () {
- var editor = new TE({model: view.model, fields: view.options.fields});
+ var editor = new TE({
+ model: view.model,
+ fields: view.options.fields
+ });
editor.show(function (model) {
view.render();
});
@@ -660,6 +701,7 @@
var comment_editor = null;
TheTicket = new MTrackTicket(base_ticket);
+ TheTicket.getAttachments().reset(attachments);
var V = new TV({
model: TheTicket,
https://bitbucket.org/wez/mtrack/changeset/06df839b1e43/
changeset: 06df839b1e43
user: wez
date: 2012-04-25 07:13:00
summary: tweak the presentation a bit
affected #: 3 files
diff -r e5f36d717491ac8629b90152d61bbee0ea1bc461 -r 06df839b1e435fcba4b6a87a35ddd69148b9cdf4 web/js/templates/ticket.show.html
--- a/web/js/templates/ticket.show.html
+++ b/web/js/templates/ticket.show.html
@@ -1,7 +1,7 @@
<style>
section {
/* compensate for navbar offset for id=XXX anchor targets */
- padding-top: 2.5em;
+ padding-top: 5em;
}
div#tkt-description {
border: solid 1px #ddd;
@@ -10,12 +10,18 @@
}
div.tkt-outline {
position: fixed;
- right: 0.5em;
+ right: 0px;
+ left: 0px;
top: 3em;
- width: 10em;
- min-height: 10em;
border: solid 1px #ddd;
- background-color: white;
+ background-color: #eee;
+ }
+ div.tkt-outline ul#tkt-nav {
+ margin: 0px;
+ }
+ div.tkt-outline ul#tkt-nav .pull-right {
+ float: right;
+ margin-right: 1em;
}
#tkt-fields table tr td.fieldname {
text-align: right;
@@ -28,12 +34,13 @@
</style><div class='tkt-outline'>
- <button class='btn btn-primary'><i class='icon-pencil icon-white'></i> Edit</button><ul id='tkt-nav' class='nav nav-pills'>
- <li class='active'><a href='#fields'>Summary</a></li>
+ <li><a href='#fields'>Summary</a></li><li><a href='#description'>Description</a></li><li><a href='#attach'>Attachments</a></li><li><a href='#comments'>Comments</a></li>
+ <li class='pull-right'><a href='#' class='btn' id='editbtn'><i class='icon-pencil'></i>
+ Edit</a></li></ul></div>
diff -r e5f36d717491ac8629b90152d61bbee0ea1bc461 -r 06df839b1e435fcba4b6a87a35ddd69148b9cdf4 web/js/templates/user.name.html
--- a/web/js/templates/user.name.html
+++ b/web/js/templates/user.name.html
@@ -1,2 +1,2 @@
<a href="<%= ABSWEB %>user.php/<%- id %>"
- ><img class='gravatar' src="<%= ABSWEB %>avatar.php?u=<%- id %>&s=24"><%- label %></a>
+ ><img class='gravatar' height='24' width=24' src="<%= ABSWEB %>avatar.php?u=<%- id %>&s=24"><%- label %></a>
diff -r e5f36d717491ac8629b90152d61bbee0ea1bc461 -r 06df839b1e435fcba4b6a87a35ddd69148b9cdf4 web/ticket2.php
--- a/web/ticket2.php
+++ b/web/ticket2.php
@@ -677,7 +677,7 @@
// Open the ticket editor
var view = this;
- $('button.btn-primary', this.el).click(function () {
+ $('#editbtn', this.el).click(function () {
var editor = new TE({
model: view.model,
fields: view.options.fields
@@ -685,6 +685,7 @@
editor.show(function (model) {
view.render();
});
+ return false;
});
return this;
}
https://bitbucket.org/wez/mtrack/changeset/9cf9fdc6c9fd/
changeset: 9cf9fdc6c9fd
user: wez
date: 2012-04-25 14:25:55
summary: show attachments and changes in the ticket view.
trap validation errors in the editor
affected #: 5 files
diff -r 06df839b1e435fcba4b6a87a35ddd69148b9cdf4 -r 9cf9fdc6c9fd11f4a1918694ad6b6eaac047d18c web/js/templates/attachment.item.edit.html
--- /dev/null
+++ b/web/js/templates/attachment.item.edit.html
@@ -0,0 +1,17 @@
+<div class='attachment'>
+ <a class='attachment' href='<%= ABSWEB %>attachment.php/<%- object %>/<%- cid %>/<%- filename %>'><%- filename %></a> (<%- size %>) added by <%- who %>
+ <abbr class='timeinterval' title='<%- changedate %>'><%- changedate %></abbr>
+ <button class='btn btn-mini delattach'><i class='icon-trash'></i></button>
+ <% if (image) {
+ var w = parseInt(width);
+ var h = parseInt(height);
+ var mw = 500;
+ if (w > mw) {
+ var s = w / mw;
+ height = h / s;
+ width = mw;
+ }
+ %>
+ <br><a href='<%= ABSWEB %>attachment.php/<%- object %>/<%- cid %>/<%- filename %>'><img src='<%= ABSWEB %>attachment.php/<%- object %>/<%- cid %>/<%- filename %>' width='<%- width %>' height='<%- height %>' border='0'></a>
+ <% } %>
+</div>
diff -r 06df839b1e435fcba4b6a87a35ddd69148b9cdf4 -r 9cf9fdc6c9fd11f4a1918694ad6b6eaac047d18c web/js/templates/attachment.item.html
--- a/web/js/templates/attachment.item.html
+++ b/web/js/templates/attachment.item.html
@@ -1,7 +1,6 @@
<div class='attachment'><a class='attachment' href='<%= ABSWEB %>attachment.php/<%- object %>/<%- cid %>/<%- filename %>'><%- filename %></a> (<%- size %>) added by <%- who %><abbr class='timeinterval' title='<%- changedate %>'><%- changedate %></abbr>
- <button class='btn btn-mini delattach'><i class='icon-trash'></i></button><% if (image) {
var w = parseInt(width);
var h = parseInt(height);
diff -r 06df839b1e435fcba4b6a87a35ddd69148b9cdf4 -r 9cf9fdc6c9fd11f4a1918694ad6b6eaac047d18c web/js/templates/ticket.event.show.html
--- /dev/null
+++ b/web/js/templates/ticket.event.show.html
@@ -0,0 +1,65 @@
+ <div class='ticketevent'>
+ <a class='pmark' href='#<%- id %>'>#</a><a name='<%- id %>'> </a><abbr class='timeinterval' title='<%- changedate %>'><%- changedate %></abbr><%- who %>
+ <a class='replycomment' href="javascript:mtrack_reply_comment(<%- id %>);">reply</a>
+ </div>
+ <div class='ticketchangeinfo'>
+ <img class='gravatar' src="<%= ABSWEB %>avatar.php?u=<%- who %>&s=48">
+ <%
+ var comment = null;
+
+ _.each(audit, function (ent) {
+ if (ent.label == 'Nsident') {
+ return;
+ }
+ if (ent.label == 'Comment') {
+ comment = ent.value_html;
+ return;
+ }
+
+ if (ent.action == 'deleted') {
+ %><b><%- ent.label %></b><%- ent.action %><%
+ } else if (ent.label != 'Description') {
+ if (_.isObject(ent.value)) {
+ %>
+ <b><%- ent.label %></b> →
+ <%
+ var cls = ent.label.toLowerCase();
+ var url = null;
+ if (cls == 'milestone') {
+ url = ABSWEB + 'milestone.php/';
+ }
+ if (cls == 'keyword') {
+ url = ABSWEB + 'search.php?q=keyword:';
+ }
+ if (cls == 'dependencies' || cls == 'blocks' ||
+ cls == 'children' || cls == 'parent') {
+ cls = 'ticketlink';
+ url = ABSWEB + 'ticket.php/';
+ }
+ for (var id in ent.value) {
+ if (url) {
+ %><span class="<%- cls %>"><a href="<%- url %><%- ent.value[id] %>"><%- ent.value[id] %></a></span><%
+ } else {
+ %><span class="<%- cls %>"><%- ent.value[id] %></span><%
+ }
+ }
+ } else {
+ %>
+ <b><%- ent.label %></b> → <%- ent.value %>
+ <%
+ }
+ } else {
+ %>
+ <b><%- ent.label %></b><%- ent.action %><button class="btn toggle-desc" desc-id="desc-<%- ent.cid %>">Toggle</button>
+ <p id="desc-<%- ent.cid %>" class="hide-desc"><%- ent.value %></p>
+ <%
+ }
+ %>
+
+ <br/>
+ <%
+ });
+ if (comment) { print(comment); }
+ %>
+ </div>
+
diff -r 06df839b1e435fcba4b6a87a35ddd69148b9cdf4 -r 9cf9fdc6c9fd11f4a1918694ad6b6eaac047d18c web/js/templates/ticket.show.html
--- a/web/js/templates/ticket.show.html
+++ b/web/js/templates/ticket.show.html
@@ -3,6 +3,11 @@
/* compensate for navbar offset for id=XXX anchor targets */
padding-top: 5em;
}
+ /* similarly, extra compensation for <a name> as we have an extra
+ * nav bar */
+ a[name] {
+ padding-top: 2em;
+ }
div#tkt-description {
border: solid 1px #ddd;
margin: -0.5em;
@@ -68,11 +73,12 @@
<section id='attach'><div id='tkt-attach'><h2>Attachments</h2>
+ <div id="attach-list"></div></div></section><section id='comments'>
+ <h2>Comments</h2><div id='tkt-comments'>
- <h2>Comments</h2></div></section>
diff -r 06df839b1e435fcba4b6a87a35ddd69148b9cdf4 -r 9cf9fdc6c9fd11f4a1918694ad6b6eaac047d18c web/ticket2.php
--- a/web/ticket2.php
+++ b/web/ticket2.php
@@ -77,6 +77,11 @@
}
});
+ // Validation errors
+ dup_model.bind('error', function (model, err) {
+ mtrack_ajax_error_to_dom(err, $('div.modal-header', view.el));
+ });
+
var attach_list = $('#attach-list', dlg);
attach_list.on('click', 'button.delattach', function() {
var att = $(this).closest('div.attachment').data('attachment-model');
@@ -96,7 +101,7 @@
});
function redraw_attachment_list() {
attach_list.empty();
- var t = _.template(mtrack_underscore_templates['attachment-item']);
+ var t = _.template(mtrack_underscore_templates['attachment-item-edit']);
dup_model.getAttachments().each(function (att) {
var o = att.toJSON();
@@ -547,7 +552,7 @@
}
if (!is_conflict) {
- mtrack_ajax_error_to_dom(resp, $('div.modal-body', dlg));
+ mtrack_ajax_error_to_dom(resp, $('div.modal-header', dlg));
return;
}
@@ -577,6 +582,68 @@
var model = this.model;
+ var attach_list = $('#attach-list', this.el);
+ function redraw_attachment_list() {
+ attach_list.empty();
+ var t = _.template(mtrack_underscore_templates['attachment-item']);
+
+ model.getAttachments().each(function (att) {
+ var o = att.toJSON();
+ if ('width' in o) {
+ o.image = true;
+ } else {
+ o.image = false;
+ }
+ var d = $(t(o));
+ d.data('attachment-model', att);
+ attach_list.append(d);
+ });
+ $('.timeinterval', attach_list).timeago();
+ }
+ redraw_attachment_list();
+ model.getAttachments().bind('all', function () {
+ redraw_attachment_list();
+ });
+
+ var change_tpl = _.template(
+ mtrack_underscore_templates['ticket-event-show']);
+ var change_cont = $('#tkt-comments', this.el);
+
+ function add_one_change(cs) {
+ var d = $('<div/>');
+ $(d).html(change_tpl(cs.toJSON()));
+ var had_comment = false;
+ var commit = false;
+ _.each(cs.get('audit'), function (a) {
+ if (a.label == 'Comment') {
+ had_comment = true;
+ if (a.value.match(/^\(In /)) {
+ commit = true;
+ }
+ }
+ });
+ if (had_comment) {
+ d.addClass('chg-comment');
+ if (commit) {
+ d.addClass('chg-commit');
+ }
+ } else {
+ d.addClass('chg-no-comment');
+ }
+ d.appendTo(change_cont);
+ $('.toggle-desc', d).click(function () {
+ $('#' + $(this).attr('desc-id')).toggle();
+ return false;
+ });
+ $('.timeinterval', d).timeago();
+ }
+ function redraw_change_list() {
+ model.getChanges().each(function (cs) {
+ add_one_change(cs);
+ });
+ }
+ redraw_change_list();
+
var user_template = _.template(mtrack_underscore_templates['user-name']);
function render_user(user) {
if (!_.isObject(user)) {
@@ -703,6 +770,7 @@
TheTicket = new MTrackTicket(base_ticket);
TheTicket.getAttachments().reset(attachments);
+ TheTicket.getChanges().reset(changes);
var V = new TV({
model: TheTicket,
https://bitbucket.org/wez/mtrack/changeset/86728bbd5710/
changeset: 86728bbd5710
user: wez
date: 2012-04-25 14:29:26
summary: nicer styling for edit button
affected #: 1 file
diff -r 9cf9fdc6c9fd11f4a1918694ad6b6eaac047d18c -r 86728bbd57103eda317b1f84691a191348c68540 web/js/templates/ticket.show.html
--- a/web/js/templates/ticket.show.html
+++ b/web/js/templates/ticket.show.html
@@ -28,6 +28,9 @@
float: right;
margin-right: 1em;
}
+ div.tkt-outline ul a.btn {
+ padding: 4px 10px;
+ }
#tkt-fields table tr td.fieldname {
text-align: right;
font-weight: bold;
https://bitbucket.org/wez/mtrack/changeset/f7209d54865d/
changeset: f7209d54865d
user: wez
date: 2012-04-25 15:08:21
summary: wire up change refreshing. Want to make the network part of this more
efficient by asking for changes since a cid.
affected #: 1 file
diff -r 86728bbd57103eda317b1f84691a191348c68540 -r f7209d54865d44730496c8fab76c156b9e58c55c web/ticket2.php
--- a/web/ticket2.php
+++ b/web/ticket2.php
@@ -74,6 +74,9 @@
dlg.on('hidden', function () {
if (!in_wiki) {
$(view.el).remove();
+ if ('hidden' in on_success) {
+ on_success.hidden();
+ }
}
});
@@ -533,7 +536,9 @@
$('.modal-footer button.btn-primary', dlg).click(function () {
view.model.save(dup_model.attributes, {
success: function(model) {
- on_success(model);
+ if ('success' in on_success) {
+ on_success.success(model);
+ }
dlg.modal('hide');
},
error: function (model, resp) {
@@ -581,6 +586,7 @@
TR.remove();
var model = this.model;
+ var view = this;
var attach_list = $('#attach-list', this.el);
function redraw_attachment_list() {
@@ -644,6 +650,53 @@
}
redraw_change_list();
+ function refresh_changes() {
+ var changes = model.getChanges();
+ var recent =
changes.at(0);
+
+ // If a modal dialog is open, don't do any updating
+ if ($('body').hasClass('modal-open')) {
+ return;
+ }
+
+ if (!recent) {
+ return;
+ }
+
+ changes.fetch({
+ success: function (c, r) {
+ if (!r.length) {
+ return;
+ }
+ var latest =
changes.at(0);
+ if (
latest.id ==
recent.id) {
+ // No change
+ return;
+ }
+ model.getAttachments().fetch();
+ var base_cid = model.get('updated').cid;
+ model.fetch({
+ success: function (model, resp) {
+ if (model.get('updated').cid == base_cid) {
+ return;
+ }
+ /* it changed */
+ model.unset('comment', {silent: true});
+ view.render();
+ }
+ });
+ }
+ });
+ }
+ function disable_change_refresh() {
+ clearInterval(view.timer);
+ view.timer = null;
+ }
+ function enable_change_refresh() {
+ view.timer = setInterval(refresh_changes, 60000);
+ }
+ enable_change_refresh();
+
var user_template = _.template(mtrack_underscore_templates['user-name']);
function render_user(user) {
if (!_.isObject(user)) {
@@ -742,15 +795,20 @@
}
$('.timeinterval', this.el).timeago();
+
// Open the ticket editor
- var view = this;
$('#editbtn', this.el).click(function () {
var editor = new TE({
model: view.model,
fields: view.options.fields
});
- editor.show(function (model) {
- view.render();
+ editor.show({
+ success: function (model) {
+ view.render();
+ },
+ hidden: function () {
+ refresh_changes();
+ }
});
return false;
});
@@ -772,6 +830,11 @@
TheTicket.getAttachments().reset(attachments);
TheTicket.getChanges().reset(changes);
+ TheTicket.bind('change:summary', function() {
+ $('html head title').text('#' + TheTicket.get('nsident') + ' ' +
+ TheTicket.get('summary'));
+ });
+
var V = new TV({
model: TheTicket,
fields: FIELDSET,
https://bitbucket.org/wez/mtrack/changeset/09d846194b6b/
changeset: 09d846194b6b
user: wez
date: 2012-04-25 15:09:41
summary: turn off replies in the new ticket page, until they are actually implemented
affected #: 1 file
diff -r f7209d54865d44730496c8fab76c156b9e58c55c -r 09d846194b6b30aece5cc23d958849dbd4c6f937 web/js/templates/ticket.event.show.html
--- a/web/js/templates/ticket.event.show.html
+++ b/web/js/templates/ticket.event.show.html
@@ -1,6 +1,7 @@
<div class='ticketevent'><a class='pmark' href='#<%- id %>'>#</a><a name='<%- id %>'> </a><abbr class='timeinterval' title='<%- changedate %>'><%- changedate %></abbr><%- who %>
- <a class='replycomment' href="javascript:mtrack_reply_comment(<%- id %>);">reply</a>
+ <!-- a class='replycomment'
+ href="javascript:mtrack_reply_comment(<%- id %>);">reply</a --></div><div class='ticketchangeinfo'><img class='gravatar' src="<%= ABSWEB %>avatar.php?u=<%- who %>&s=48">
https://bitbucket.org/wez/mtrack/changeset/26a7dc530a2e/
changeset: 26a7dc530a2e
user: wez
date: 2012-04-25 20:52:49
summary: add templates for more field types
affected #: 3 files
diff -r 09d846194b6b30aece5cc23d958849dbd4c6f937 -r 26a7dc530a2e2f11c8c6092bdab3ef9eb0371a3d web/js/templates/ticket.field.bytype.multiselect.html
--- /dev/null
+++ b/web/js/templates/ticket.field.bytype.multiselect.html
@@ -0,0 +1,4 @@
+<% _.each(value, function (label, id) { %>
+ <%- label %>
+<% }); %>
+
diff -r 09d846194b6b30aece5cc23d958849dbd4c6f937 -r 26a7dc530a2e2f11c8c6092bdab3ef9eb0371a3d web/js/templates/ticket.field.bytype.tags.html
--- /dev/null
+++ b/web/js/templates/ticket.field.bytype.tags.html
@@ -0,0 +1,5 @@
+<% _.each(value, function (label, id) { %>
+<a class='keyword'
+ href="<%= ABSWEB %>search.php/q=keyword:<%- label %>"><%- label %></a>
+<% }); %>
+
diff -r 09d846194b6b30aece5cc23d958849dbd4c6f937 -r 26a7dc530a2e2f11c8c6092bdab3ef9eb0371a3d web/js/templates/ticket.field.bytype.ticketdeps.html
--- /dev/null
+++ b/web/js/templates/ticket.field.bytype.ticketdeps.html
@@ -0,0 +1,4 @@
+<% _.each(value, function (nsident, tid) { %>
+<a class='ticketlink'
+ href="<%= ABSWEB %>ticket.php/<%- nsident %>">#<%- nsident %></a>
+<% }); %>
https://bitbucket.org/wez/mtrack/changeset/2521ba07d8cf/
changeset: 2521ba07d8cf
user: wez
date: 2012-04-26 03:00:50
summary: pull field summary over to the right and have it float there.
affected #: 3 files
diff -r 26a7dc530a2e2f11c8c6092bdab3ef9eb0371a3d -r 2521ba07d8cf3d94d6c3bc364b37da1dad11c8e1 web/js/templates/item.changed.html
--- a/web/js/templates/item.changed.html
+++ b/web/js/templates/item.changed.html
@@ -1,3 +1,3 @@
<img class='gravatar' src="<%= ABSWEB %>avatar.php?u=<%- who %>&s=24">
-<abbr class='timeinterval' title='<%- when %>'><%- when %></abbr> by
- <a href="<%= ABSWEB %>user.php/<%- who %>"><%- who %></a>
+<abbr class='timeinterval' title='<%- when %>'><%- when %></abbr><br>
+by <a href="<%= ABSWEB %>user.php/<%- who %>"><%- who %></a>
diff -r 26a7dc530a2e2f11c8c6092bdab3ef9eb0371a3d -r 2521ba07d8cf3d94d6c3bc364b37da1dad11c8e1 web/js/templates/ticket.show.html
--- a/web/js/templates/ticket.show.html
+++ b/web/js/templates/ticket.show.html
@@ -8,6 +8,10 @@
a[name] {
padding-top: 2em;
}
+ a[name=description] {
+ top: 0;
+ padding-top: 8em;
+ }
div#tkt-description {
border: solid 1px #ddd;
margin: -0.5em;
@@ -20,6 +24,7 @@
top: 3em;
border: solid 1px #ddd;
background-color: #eee;
+ z-index: 10;
}
div.tkt-outline ul#tkt-nav {
margin: 0px;
@@ -31,47 +36,72 @@
div.tkt-outline ul a.btn {
padding: 4px 10px;
}
+ #tkt-fields {
+ position: fixed;
+ background-color: #eee;
+ right: 0;
+ top: 5.0em;
+ padding: 1em;
+ width: 20em;
+ z-index: 9;
+ border: solid 1px #ddd;
+ }
+ #tkt-fields table tr td {
+ padding-top: 0.2em;
+ padding-bottom: 0.2em;
+ }
#tkt-fields table tr td.fieldname {
text-align: right;
font-weight: bold;
+ font-size: 0.8em;
+ white-space: nowrap;
color: #777;
+ max-width: 8.5em;
+ overflow: hidden;
+ text-overflow: ellipsis;
}
#tkt-fields img.gravatar {
vertical-align: middle;
}
+ h1.ticket-summary {
+ margin-top: 1em;
+ }
+ h1.ticket-summary.closed {
+ text-decoration: line-through;
+ color: #777;
+ }
</style><div class='tkt-outline'><ul id='tkt-nav' class='nav nav-pills'>
- <li><a href='#fields'>Summary</a></li><li><a href='#description'>Description</a></li><li><a href='#attach'>Attachments</a></li><li><a href='#comments'>Comments</a></li>
- <li class='pull-right'><a href='#' class='btn' id='editbtn'><i class='icon-pencil'></i>
- Edit</a></li>
+ <li class='pull-right'><a href='#' class='btn'
+ id='togglebtn'>Toggle Fields</a></li>
+ <li class='pull-right'><a href='#' class='btn'
+ id='editbtn'><i class='icon-pencil'></i> Edit</a></li></ul></div>
-<section id='fields'>
+<div id='tkt-fields' class='tkt-fields'>
+ <table>
+ <tr>
+ <td class='fieldname'></td>
+ <td class='fieldvalue'></td>
+ </tr>
+ </table>
+</div>
+
+<a name='description'>X</a><h1 class="ticket-summary <% if (status == 'closed') { %>closed<% } %>"><% if (nsident) { %>
#<%- nsident %><% } else { %>
[NEW]
<% } %><%- summary %></h1>
- <div id='tkt-fields' class='tkt-fields'>
- <table>
- <tr>
- <td class='fieldname'></td>
- <td class='fieldvalue'></td>
- </tr>
- </table>
- </div>
-</section>
-<section id='description'><div class="wikipage" id="tkt-description"><%= description_html %></div>
-</section><section id='attach'><div id='tkt-attach'>
diff -r 26a7dc530a2e2f11c8c6092bdab3ef9eb0371a3d -r 2521ba07d8cf3d94d6c3bc364b37da1dad11c8e1 web/ticket2.php
--- a/web/ticket2.php
+++ b/web/ticket2.php
@@ -746,7 +746,7 @@
return;
}
var tr = TR.clone();
- $('td.fieldname', tr).text(field.label);
+ $('td.fieldname', tr).text(field.label).attr('title', field.label);
if (
field.name in renderers) {
$('td.fieldvalue', tr).html(renderers[
field.name](val));
} else {
@@ -783,6 +783,12 @@
table.append(tr);
}
+ if (model.get('status') == 'closed') {
+ process_field({name: 'resolution', label: 'Resolved'});
+ } else {
+ process_field({name: 'status', label: 'Status'});
+ }
+
process_field({name: 'created', label: 'Opened'});
if (model.get('updated') &&
model.get('updated').cid != model.get('created').cid) {
@@ -795,6 +801,10 @@
}
$('.timeinterval', this.el).timeago();
+ $('#togglebtn', this.el).click(function () {
+ $('#tkt-fields', this.el).toggle();
+ return false;
+ });
// Open the ticket editor
$('#editbtn', this.el).click(function () {
https://bitbucket.org/wez/mtrack/changeset/587d5aa70683/
changeset: 587d5aa70683
user: wez
date: 2012-04-26 03:23:03
summary: show parent ticket. move avatars to left side.
affected #: 4 files
diff -r 2521ba07d8cf3d94d6c3bc364b37da1dad11c8e1 -r 587d5aa70683fc99d1c62c7dbc0d859f44c68616 inc/issue.php
--- a/inc/issue.php
+++ b/inc/issue.php
@@ -1742,6 +1742,10 @@
if (strncmp($k, "x_", 2)) continue;
$j->$k = $v;
}
+ if ($tkt->ptid) {
+ $p = self::loadById($tkt->ptid);
+ $j->parent = $p->nsident;
+ }
$j->cc = $tkt->getCc();
$j->keywords = $tkt->getKeywords();
$j->components = $tkt->getComponents();
diff -r 2521ba07d8cf3d94d6c3bc364b37da1dad11c8e1 -r 587d5aa70683fc99d1c62c7dbc0d859f44c68616 web/js/templates/ticket.field.bytype.ticket.html
--- /dev/null
+++ b/web/js/templates/ticket.field.bytype.ticket.html
@@ -0,0 +1,2 @@
+<a class='ticketlink'
+ href="<%= ABSWEB %>ticket.php/<%- value %>">#<%- value %></a>
diff -r 2521ba07d8cf3d94d6c3bc364b37da1dad11c8e1 -r 587d5aa70683fc99d1c62c7dbc0d859f44c68616 web/js/templates/ticket.show.html
--- a/web/js/templates/ticket.show.html
+++ b/web/js/templates/ticket.show.html
@@ -16,6 +16,8 @@
border: solid 1px #ddd;
margin: -0.5em;
padding: 0.5em;
+ background-color: #ffc;
+ min-height: 5em;
}
div.tkt-outline {
position: fixed;
@@ -70,6 +72,18 @@
text-decoration: line-through;
color: #777;
}
+ div#tkt-comments .ticketchangeinfo {
+ padding-left: 60px;
+ min-height: 64px;
+ }
+ div#tkt-comments div.ticketevent {
+ border-bottom: solid 1px #eee;
+ }
+ div#tkt-comments .ticketchangeinfo img.gravatar {
+ float: none;
+ position: absolute;
+ left: 0px;
+ }
</style><div class='tkt-outline'>
diff -r 2521ba07d8cf3d94d6c3bc364b37da1dad11c8e1 -r 587d5aa70683fc99d1c62c7dbc0d859f44c68616 web/ticket2.php
--- a/web/ticket2.php
+++ b/web/ticket2.php
@@ -788,6 +788,9 @@
} else {
process_field({name: 'status', label: 'Status'});
}
+ if (model.get('parent')) {
+ process_field({name: 'parent', label: 'Parent', type: 'ticket'});
+ }
process_field({name: 'created', label: 'Opened'});
if (model.get('updated') &&
https://bitbucket.org/wez/mtrack/changeset/c063907c3b94/
changeset: c063907c3b94
user: wez
date: 2012-04-26 04:37:45
summary: add ticket splitting capability
affected #: 4 files
diff -r 587d5aa70683fc99d1c62c7dbc0d859f44c68616 -r c063907c3b9411dd6a2718874351a29a9e5252aa web/css/tw-bootstrap.css
--- a/web/css/tw-bootstrap.css
+++ b/web/css/tw-bootstrap.css
@@ -331,6 +331,161 @@
*padding-top: 1px;
*padding-bottom: 1px;
}
+.btn-group {
+ position: relative;
+ *zoom: 1;
+ *margin-left: .3em;
+}
+.btn-group:before,
+.btn-group:after {
+ display: table;
+ content: "";
+}
+.btn-group:after {
+ clear: both;
+}
+.btn-group:first-child {
+ *margin-left: 0;
+}
+.btn-group + .btn-group {
+ margin-left: 5px;
+}
+.btn-toolbar {
+ margin-top: 9px;
+ margin-bottom: 9px;
+}
+.btn-toolbar .btn-group {
+ display: inline-block;
+ *display: inline;
+ /* IE7 inline-block hack */
+
+ *zoom: 1;
+}
+.btn-group .btn {
+ position: relative;
+ float: left;
+ margin-left: -1px;
+ -webkit-border-radius: 0;
+ -moz-border-radius: 0;
+ border-radius: 0;
+}
+.btn-group .btn:first-child {
+ margin-left: 0;
+ -webkit-border-top-left-radius: 4px;
+ -moz-border-radius-topleft: 4px;
+ border-top-left-radius: 4px;
+ -webkit-border-bottom-left-radius: 4px;
+ -moz-border-radius-bottomleft: 4px;
+ border-bottom-left-radius: 4px;
+}
+.btn-group .btn:last-child,
+.btn-group .dropdown-toggle {
+ -webkit-border-top-right-radius: 4px;
+ -moz-border-radius-topright: 4px;
+ border-top-right-radius: 4px;
+ -webkit-border-bottom-right-radius: 4px;
+ -moz-border-radius-bottomright: 4px;
+ border-bottom-right-radius: 4px;
+}
+.btn-group .btn.large:first-child {
+ margin-left: 0;
+ -webkit-border-top-left-radius: 6px;
+ -moz-border-radius-topleft: 6px;
+ border-top-left-radius: 6px;
+ -webkit-border-bottom-left-radius: 6px;
+ -moz-border-radius-bottomleft: 6px;
+ border-bottom-left-radius: 6px;
+}
+.btn-group .btn.large:last-child,
+.btn-group .large.dropdown-toggle {
+ -webkit-border-top-right-radius: 6px;
+ -moz-border-radius-topright: 6px;
+ border-top-right-radius: 6px;
+ -webkit-border-bottom-right-radius: 6px;
+ -moz-border-radius-bottomright: 6px;
+ border-bottom-right-radius: 6px;
+}
+.btn-group .btn:hover,
+.btn-group .btn:focus,
+.btn-group .btn:active,
+.btn-group .btn.active {
+ z-index: 2;
+}
+.btn-group .dropdown-toggle:active,
+.btn-group.open .dropdown-toggle {
+ outline: 0;
+}
+.btn-group .dropdown-toggle {
+ padding-left: 8px;
+ padding-right: 8px;
+ -webkit-box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
+ -moz-box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
+ box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
+ *padding-top: 3px;
+ *padding-bottom: 3px;
+}
+.btn-group .btn-mini.dropdown-toggle {
+ padding-left: 5px;
+ padding-right: 5px;
+ *padding-top: 1px;
+ *padding-bottom: 1px;
+}
+.btn-group .btn-small.dropdown-toggle {
+ *padding-top: 4px;
+ *padding-bottom: 4px;
+}
+.btn-group .btn-large.dropdown-toggle {
+ padding-left: 12px;
+ padding-right: 12px;
+}
+.btn-group.open {
+ *z-index: 1000;
+}
+.btn-group.open .dropdown-menu {
+ display: block;
+ margin-top: 1px;
+ -webkit-border-radius: 5px;
+ -moz-border-radius: 5px;
+ border-radius: 5px;
+}
+.btn-group.open .dropdown-toggle {
+ background-image: none;
+ -webkit-box-shadow: inset 0 1px 6px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05);
+ -moz-box-shadow: inset 0 1px 6px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05);
+ box-shadow: inset 0 1px 6px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05);
+}
+.btn .caret {
+ margin-top: 7px;
+ margin-left: 0;
+}
+.btn:hover .caret,
+.open.btn-group .caret {
+ opacity: 1;
+ filter: alpha(opacity=100);
+}
+.btn-mini .caret {
+ margin-top: 5px;
+}
+.btn-small .caret {
+ margin-top: 6px;
+}
+.btn-large .caret {
+ margin-top: 6px;
+ border-left: 5px solid transparent;
+ border-right: 5px solid transparent;
+ border-top: 5px solid #000000;
+}
+.btn-primary .caret,
+.btn-warning .caret,
+.btn-danger .caret,
+.btn-info .caret,
+.btn-success .caret,
+.btn-inverse .caret {
+ border-top-color: #ffffff;
+ border-bottom-color: #ffffff;
+ opacity: 0.75;
+ filter: alpha(opacity=75);
+}
.modal-open .dropdown-menu {
z-index: 2050;
diff -r 587d5aa70683fc99d1c62c7dbc0d859f44c68616 -r c063907c3b9411dd6a2718874351a29a9e5252aa web/js/templates/ticket.edit.html
--- a/web/js/templates/ticket.edit.html
+++ b/web/js/templates/ticket.edit.html
@@ -67,7 +67,11 @@
</tr></table></div>
+
<div class='tab-pane' id='tkt-edit-attachments'>
+ <% if (isnew) { %>
+ <p>Attachments available after saving</p>
+ <% } else { %><div id='attach-list'></div><form action="<%= ABSWEB %>post-attachment.php" method="POST"
@@ -82,6 +86,7 @@
<input type="submit" class='btn'
id="confirm-upload" value="Upload"></form>
+ <% } %></div></div></div>
diff -r 587d5aa70683fc99d1c62c7dbc0d859f44c68616 -r c063907c3b9411dd6a2718874351a29a9e5252aa web/js/templates/ticket.show.html
--- a/web/js/templates/ticket.show.html
+++ b/web/js/templates/ticket.show.html
@@ -84,6 +84,20 @@
position: absolute;
left: 0px;
}
+ div.tkt-outline span.caret {
+ border-top-color: black !important;
+ border-bottom-color: black !important;
+ }
+ div.tkt-outline div.btn-group {
+ padding: 2px;
+ }
+ div.tkt-outline div.btn-group a {
+ padding: 5px 10px;
+ }
+ div.tkt-outline #togglebtn {
+ padding: 5px 10px;
+ padding-bottom: 4px;
+ }
</style><div class='tkt-outline'>
@@ -95,6 +109,20 @@
id='togglebtn'>Toggle Fields</a></li><li class='pull-right'><a href='#' class='btn'
id='editbtn'><i class='icon-pencil'></i> Edit</a></li>
+ <li class='pull-right'>
+ <div class='btn-group'>
+ <a class='btn dropdown-toggle' data-toggle='dropdown' href='#'>
+ Split
+ <span class='caret'></span>
+ </a>
+ <ul class='dropdown-menu'>
+ <% if (!ptid) { %>
+ <li><a href='#' id='splitchild'>Split and make a child task</a></li>
+ <% } %>
+ <li><a href='#' id='splitsib'>Split and make a sibling task</a></li>
+ </ul>
+ </div>
+ </li></ul></div>
@@ -113,7 +141,7 @@
#<%- nsident %><% } else { %>
[NEW]
- <% } %><%- summary %></h1>
+ <% } %><%- summary ? summary : '' %></h1><div class="wikipage" id="tkt-description"><%= description_html %></div>
diff -r 587d5aa70683fc99d1c62c7dbc0d859f44c68616 -r c063907c3b9411dd6a2718874351a29a9e5252aa web/ticket2.php
--- a/web/ticket2.php
+++ b/web/ticket2.php
@@ -592,8 +592,10 @@
function redraw_attachment_list() {
attach_list.empty();
var t = _.template(mtrack_underscore_templates['attachment-item']);
+ var count = 0;
model.getAttachments().each(function (att) {
+ count++;
var o = att.toJSON();
if ('width' in o) {
o.image = true;
@@ -604,7 +606,14 @@
d.data('attachment-model', att);
attach_list.append(d);
});
- $('.timeinterval', attach_list).timeago();
+ if (count) {
+ $('.timeinterval', attach_list).timeago();
+ $('#attach', view.el).show();
+ $('.tkt-outline a[href=#attach]').show();
+ } else {
+ $('#attach', view.el).hide();
+ $('.tkt-outline a[href=#attach]').hide();
+ }
}
redraw_attachment_list();
model.getAttachments().bind('all', function () {
@@ -825,6 +834,73 @@
});
return false;
});
+
+ /* operates on the result of a ticket split; if saved,
+ * we're taken to the ticket page for the newly saved ticket */
+ function edit_split(model) {
+ var editor = new TE({
+ model: model,
+ fields: view.options.fields
+ });
+ editor.show({
+ success: function (model) {
+ /* go to that ticket page */
+ window.location = ABSWEB + 'ticket.php/' + model.get('nsident');
+ },
+ hidden: function () {
+ refresh_changes();
+ }
+ });
+ return false;
+ }
+
+ function make_split_ticket() {
+ var S = view.model.clone();
+ console.log("cloned as", S);
+ S.unset('spent');
+ S.unset('remaining');
+ S.unset('estimated');
+ S.unset('nsident');
+ S.unset('id');
+ S.unset('created');
+ S.unset('updated');
+ S.set({children: []});
+ S.set({description:
+ "\\nSplit from #" + view.model.get('nsident') + "\\n\\n---\\n\\n" +
+ view.model.get('description')});
+ // Pointless to clone it in a closed state
+ if (S.get('status') == 'closed') {
+ S.set({status: 'open'});
+ }
+ S.unset('resolution');
+ return S;
+ }
+ $('#splitsib', this.el).click(function () {
+ edit_split(make_split_ticket());
+ return false;
+ });
+ $('#splitchild', this.el).click(function () {
+ var S = make_split_ticket();
+ // New ticket is a child of the current one
+ S.set({ptid:
view.model.id});
+ edit_split(S);
+ return false;
+ });
+
+ if (view.model.isNew()) {
+ var editor = new TE({
+ model: view.model,
+ fields: view.options.fields
+ });
+ editor.show({
+ success: function (model) {
+ window.location = ABSWEB + 'ticket.php/' + model.get('nsident');
+ },
+ hidden: function () {
+ refresh_changes();
+ }
+ });
+ }
return this;
}
});
https://bitbucket.org/wez/mtrack/changeset/8c5ed4e4569c/
changeset: 8c5ed4e4569c
user: wez
date: 2012-04-26 04:40:11
summary: replace old ticket page with the new one; keep the old one around during UAT
affected #: 3 files
diff -r c063907c3b9411dd6a2718874351a29a9e5252aa -r 8c5ed4e4569ce7bef9a6b4b16c82350b6c3a47f4 web/ticket.php
--- a/web/ticket.php
+++ b/web/ticket.php
@@ -51,170 +51,860 @@
}
echo <<<HTML
-<div id="attachment-form" class="popupForm" style="display:none">
- <form action="${ABSWEB}post-attachment.php" method="POST"
- id="upload-form" enctype="multipart/form-data" target="upload_target">
- <input type="hidden" name="object" value="ticket:X">
- <label for='attachments[]'>Select file(s) to be attached</label>
- <input name="attachments[]" class='btn multi' type="file">
- <iframe id="upload_target" name="upload_target" src="${ABSWEB}/mtrack.css">
- </iframe>
- <input type="submit" class='btn btn-primary' id="confirm-upload" value="Upload">
- <button class='btn' id="cancel-upload">Cancel</button>
- </form>
-</div>
-<div id="conflict-form" class="popupForm" style="display:none" -->
- <h1>Conflicting changes</h1>
- <p>Someone else has modified this ticket since you loaded the page.
- The differences between your desired version of the ticket and the
- currently saved version of the ticket are shown in the table below.
- </p>
- <br>
- <table>
- <thead>
- <tr>
- <th>Field</th><th>Yours</th><th>Theirs</th>
- </tr>
- </thead>
- <tbody id="conflict-list"></tbody>
- </table>
- <br>
- <p>
- Click one of the buttons below; you will be returned to the editor
- where you can make further changes (or cancel your changes).
- When you next click the save button, your changes will resolve this
- conflict and be applied to the ticket.
- </p>
- <button class='btn' id="conflict-keep">Keep my changes and return to editor</button>
- <button class='btn' id="conflict-take">Take their changes and return to editor</button>
-</div>
-<div id="issue-buttons">
- <div id="issue-content">
- <ul>
- <li><a href="#issue-container" class='active'>Description</a></li>
- <li><a href="#attach">Attachments</a></li>
- <li><a href="#change">Changes</a></li>
- </ul>
- </div>
- <div id="issue-controls">
- <div class='ui-state-error ui-corner-all' id='issue-error'>
- <span class='ui-icon ui-icon-alert'></span>
- <span id="issue-error-text"></span>
- </div>
- <button id="save-issue" class="btn btn-success hide-until-change">Save</button>
- <button id="cancel-issue" class="btn hide-until-change">Cancel</button>
- <button id="comment-issue" class='btn'><i class='icon-comment'></i> Comment</button>
- </div>
-</div>
-<div id="issue-container">
-<div id='commentedit' class='popupForm' style='display:none'></div>
-<div id='tktedit'></div>
-<div id="issue-desc"></div>
- <h2 id="attach">Attachments</h2>
- <div id="issue-attachments"></div>
- <h2 id="change">Changes</h2>
- <div id="issue-changes"></div>
-</div>
-<div id="issue-props"></div>
+<div id="ticket"></div>
+<script type='text/javascript'>
+// Ticket editor
+var TE = Backbone.View.extend({
+ show: function(on_success) {
+ var o = this.model.toJSON();
+ o.isnew = this.model.isNew();
+ // A clone of the model to use for editing with the existing
+ // set of editors
+ var dup_model = this.model.clone();
+ dup_model.getAttachments().reset(this.model.getAttachments().models);
-<script type="text/template" id='attach-template'>
- <a class='attachment' href='<%= ABSWEB %>attachment.php/<%- object %>/<%- cid %>/<%- filename %>'><%- filename %></a> (<%- size %>) added by <%- who %>
- <abbr class='timeinterval' title='<%- changedate %>'><%- changedate %></abbr>
- <button class='btn btn-mini'><i class='icon-trash'></i></button>
- <% if (image) {
- var w = parseInt(width);
- var h = parseInt(height);
- var mw = 500;
- if (w > mw) {
- var s = w / mw;
- height = h / s;
- width = mw;
+ $(this.el).html(_.template(
+ mtrack_underscore_templates['ticket-edit'], o));
+
+ var view = this;
+ $(view.el).appendTo('body');
+ var dlg = $('div.modal', view.el);
+ var in_wiki = false;
+
+ dlg.on('hidden', function () {
+ if (!in_wiki) {
+ $(view.el).remove();
+ if ('hidden' in on_success) {
+ on_success.hidden();
+ }
+ }
+ });
+
+ // Validation errors
+ dup_model.bind('error', function (model, err) {
+ mtrack_ajax_error_to_dom(err, $('div.modal-header', view.el));
+ });
+
+ var attach_list = $('#attach-list', dlg);
+ attach_list.on('click', 'button.delattach', function() {
+ var att = $(this).closest('div.attachment').data('attachment-model');
+ var m = $("<div class='modal fade'><div class='modal-header'><a class='close' data-dismiss='modal'>x</a><h3>Delete Attachment?</h3></div><div class='modal-body'><p><b></b></p><p>Do you really want to delete this attachment?</p><p>You cannot undo this action!</p></div><div class='modal-footer'><button class='btn' data-dismiss='modal'>Close</button><button class='btn btn-danger'>Delete</button></div></div>");
+
+ $('b', m).text(att.get('filename'));
+ $('.btn-danger', m).click(function () {
+ att.destroy();
+ m.modal('hide');
+ });
+
+ m.on('hidden', function() {
+ m.remove();
+ });
+ m.modal();
+
+ });
+ function redraw_attachment_list() {
+ attach_list.empty();
+ var t = _.template(mtrack_underscore_templates['attachment-item-edit']);
+
+ dup_model.getAttachments().each(function (att) {
+ var o = att.toJSON();
+ if ('width' in o) {
+ o.image = true;
+ } else {
+ o.image = false;
+ }
+ var d = $(t(o));
+ d.data('attachment-model', att);
+ attach_list.append(d);
+ });
+ $('.timeinterval', attach_list).timeago();
}
- %>
- <br><a href='<%= ABSWEB %>attachment.php/<%- object %>/<%- cid %>/<%- filename %>'><img src='<%= ABSWEB %>attachment.php/<%- object %>/<%- cid %>/<%- filename %>' width='<%- width %>' height='<%- height %>' border='0'></a>
- <% } %>
-</script>
+ redraw_attachment_list();
+ dup_model.getAttachments().bind('all', function () {
+ redraw_attachment_list();
+ });
-<script type="text/template" id='ticket-edit-template'>
- <h1><% if (status == 'closed') { %><del><% } %>
- <% if (nsident) { %>
- #<%- nsident %>
- <% } else { %>
- [NEW]
- <% } %><span id="tkt-summary-text"></span>
- <% if (status == 'closed') { %></del><% } %>
- </h1>
- <button class='btn' id='edit-description'>Edit Description</button>
-</script>
+ // Grab template elements and take them out of the DOM
+ var tab_ul = $('ul.nav-tabs', view.el);
+ var tab_hdr = $('li:first', tab_ul);
+ tab_hdr.remove();
+ var tab_att = $('li', tab_ul);
+ tab_att.remove(); // we'll append it at the end
+ var tab_content = $('div.tab-content', view.el);
+ var tab_body = $('div.tab-pane:first', tab_content);
+ tab_body.remove();
-<script type="text/template" id='ticket-change-template'>
- <div class='ticketevent'>
- <a class='pmark' href='#<%- id %>'>#</a><a name='<%- id %>'> </a><abbr class='timeinterval' title='<%- changedate %>'><%- changedate %></abbr><%- who %>
- <a class='replycomment' href="javascript:mtrack_reply_comment(<%- id %>);">reply</a>
- </div>
- <div class='ticketchangeinfo'>
- <img class='gravatar' src="${ABSWEB}avatar.php?u=<%- who %>&s=48">
- <%
- var comment = null;
+ var next_tab_id = 1;
+ function add_group_tab(group) {
+ var label;
+ if (
group.name == 0) {
+ label = 'Details';
+ } else {
+ label =
group.name;
+ }
- _.each(audit, function (ent) {
- if (ent.label == 'Nsident') {
- return;
+ var id = 'tab-' + next_tab_id++;
+
+ var hdr = tab_hdr.clone();
+ $('a', hdr).attr('href', '#' + id);
+ $('a', hdr).text(label);
+ tab_ul.append(hdr);
+
+ var tab = tab_body.clone();
+ tab.attr('id', id);
+ tab_content.append(tab);
+
+ if (next_tab_id == 2) {
+ // Make the first one active
+ $('a', hdr).trigger('click');
}
- if (ent.label == 'Comment') {
- comment = ent.value_html;
+
+ return tab;
+ }
+
+ function multi_editor(lcont, tab, field) {
+ var label = $('<label/>');
+ label.text(field.label);
+ tab.append(label);
+
+ var edit = $('<textarea/>', {
+ cols: field.cols,
+ rows: field.rows,
+ placeholder: field.placeholder
+ });
+ var val = dup_model.get(
field.name);
+ if (val) {
+ edit.val(val);
+ }
+ edit.on('change', function() {
+ var o = {};
+ o[
field.name] = edit.val();
+ dup_model.set(o);
+ });
+ lcont.remove();
+ tab.append(edit);
+ tab.attr('colspan', 2);
+ }
+
+ function wiki_editor(lcont, tab, field) {
+ var label = $('<label/>');
+ label.text(field.label);
+ tab.append(label);
+
+ var edit = $('<textarea/>', {
+ class: 'wiki shortwiki',
+ cols: field.cols,
+ rows: field.rows,
+ placeholder: field.placeholder
+ });
+ var val = dup_model.get(
field.name);
+ if (val) {
+ edit.val(val);
+ }
+ lcont.remove();
+ tab.append(edit);
+ var b = $('<button/>', {
+ class: 'btn'
+ });
+ b.html('<i class="icon-pencil"></i> Edit ' + field.label + ' in wiki editor');
+ tab.append(b);
+ tab.attr('colspan', 2);
+
+ dup_model.bind('change:' +
field.name, function () {
+ var val = dup_model.get(
field.name);
+ edit.val(val ? val : '');
+ });
+ edit.on('change', function() {
+ var o = {};
+ o[
field.name] = edit.val();
+ dup_model.set(o);
+ });
+
+ var wiki = new MTrackWikiTextAreaView({
+ model: dup_model,
+ wikiContext: 'ticket:',
+ use_overlay: true,
+ Caption: "Edit " + field.label,
+ OKLabel: "Accept " + field.label,
+ CancelLabel: "Abandon changes to " + field.label,
+ srcattr:
field.name,
+ renderedattr:
field.name + '_html'
+ });
+ wiki.bind('editstart', function () {
+ in_wiki = true;
+ setTimeout(function () {
+ dlg.modal('hide');
+ }, 1000);
+ });
+ wiki.bind('editend', function () {
+ in_wiki = false;
+ dlg.modal('show');
+ });
+
+ b.click(function () {
+ var o = {};
+ o[
field.name] = edit.val();
+ dup_model.set(o);
+ wiki.edit();
+ });
+ }
+
+ function text_editor(lcont, tab, field) {
+ lcont.text(field.label);
+
+ var inp = $('<input/>', {
+ type: "text",
+ name:
field.name,
+ placeholder: field.placeholder
+ });
+ var val = view.model.get(
field.name);
+ if (val) {
+ inp.val(val);
+ }
+ inp.on('change', function() {
+ var o = {};
+ o[
field.name] = inp.val();
+ dup_model.set(o);
+ });
+
+ tab.append(inp);
+ }
+
+ function multiselect_editor(lcont, tab, field) {
+ lcont.text(field.label);
+
+ var el = $('<div/>');
+ tab.append(el);
+ var view = new MTrackSelectEditorView({
+ el: el,
+ model: dup_model,
+ multiple: true,
+ srcattr:
field.name,
+ label: field.label,
+ width: '418px',
+ values: field.options,
+ defval: field["default"],
+ placeholder: field.placeholder
+ });
+ view.render();
+ }
+
+ function select_editor(lcont, tab, field) {
+ lcont.text(field.label);
+
+ var el = $('<span/>');
+ tab.append(el);
+ var view = new MTrackSelectEditorView({
+ el: el,
+ model: dup_model,
+ srcattr:
field.name,
+ label: field.label,
+ width: '418px',
+ values: field.options,
+ defval: field["default"],
+ placeholder: field.placeholder
+ });
+ view.render();
+ }
+
+ function ticketdeps_editor(lcont, tab, field) {
+ lcont.text(field.label);
+
+ var el = $('<span/>');
+ tab.append(el);
+ var view = new MTrackTicketDepEditView({
+ el: el,
+ model: dup_model,
+ srcattr:
field.name,
+ label: field.label,
+ });
+ view.render();
+ }
+
+ function tags_editor(lcont, tab, field) {
+ lcont.text(field.label);
+
+ var el = $('<span/>');
+ tab.append(el);
+ var view = new MTrackTagEditView({
+ el: el,
+ model: dup_model,
+ srcattr:
field.name,
+ label: field.label,
+ });
+ view.render();
+ }
+
+ function cc_editor(lcont, tab, field) {
+ lcont.text(field.label);
+
+ var el = $('<span/>');
+ tab.append(el);
+ var view = new MTrackCcEditView({
+ el: el,
+ model: dup_model,
+ srcattr:
field.name,
+ label: field.label,
+ });
+ view.render();
+ }
+
+ var editors = {
+ multi: multi_editor,
+ select: select_editor,
+ multiselect: multiselect_editor,
+ tags: tags_editor,
+ cc: cc_editor,
+ ticketdeps: ticketdeps_editor,
+ text: text_editor,
+ wiki: wiki_editor
+ };
+
+ function add_editor(tr, tab, field)
+ {
+ if (field.type == 'readonly') return;
+
+ var editor = text_editor;
+
+ if (field.type in editors) {
+ editor = editors[field.type];
+ }
+ tr = tr.clone();
+ var lcont = $('td.fieldname', tr);
+ var div = $('td.fieldvalue', tr);
+ tab.append(tr);
+ editor(lcont, div, field);
+ }
+
+ function add_tab_and_fields(group) {
+ var tab = add_group_tab(group);
+
+ var table = $('table', tab);
+ var tr = $('tr', table);
+ tr.remove();
+
+ if (
group.name == 0) {
+ // Synthesize some fields
+ add_editor(tr, table, {
+ name: 'summary',
+ label: 'Summary'
+ });
+ add_editor(tr, table, {
+ name: 'status',
+ label: 'Status',
+ type: 'select',
+ options: mtrack_ticket_states
+ });
+ if (dup_model.get('status') != 'closed') {
+ add_editor(tr, table, {
+ name: 'resolution',
+ label: 'Resolution',
+ placeholder: 'Resolve ticket as...',
+ type: 'select',
+ options: mtrack_resolutions
+ });
+ }
+ }
+ _.each(group.fields, function (field) {
+ add_editor(tr, table, field);
+ });
+ }
+
+ var com_tab = add_tab_and_fields({
+ name: 'Comment',
+ fields: [
+ {
+ name: 'comment',
+ label: 'Comment',
+ type: 'wiki',
+ placeholder: 'Something on your mind? Share it here!',
+ rows: 10,
+ cols: 78
+ }
+ ]
+ });
+
+ /* Create a tab for each category of field */
+ for (var gidx in this.options.fields) {
+ add_tab_and_fields(this.options.fields[gidx]);
+ }
+ tab_ul.append(tab_att);
+
+ /* handle uploads */
+ var uploading = false;
+ $('#confirm-upload', dlg).click(function () {
+ uploading = true;
+ $('#upload-form', dlg).submit();
+ });
+ $('#upload_target', dlg).on('load', function () {
+ var res = $(this).contents().find('body').text();
+ try {
+ res = JSON.parse(res);
+ if (res.status == 'success') {
+ if (uploading) {
+ $('<div class="alert alert-success">' +
+ '<a class="close" data-dismiss="alert">×</a>' +
+ 'Upload successful</div>').
+ appendTo($('#tkt-edit-attachments', dlg));
+ dup_model.getAttachments().reset(res.attachments);
+ }
+ $('input[type=file]', dlg).val('');
+ } else {
+ $('<div class="alert alert-danger">' +
+ '<a class="close" data-dismiss="alert">×</a>' +
+ res.message + '</div>').
+ appendTo($('#tkt-edit-attachments', dlg));
+ }
+ } catch (e) {
+ }
+ uploading = false;
+ });
+
+ // Present the conflict resolution UI.
+ // This is an alternative form that shows the changes side-by-side
+ // and allows the user to pick a resolution:
+ // - Accept my changes
+ // - Accept their changes
+ // - Cancel
+ // The first two will re-display the edit dialog with the updated
+ // model, the latter will cancel the edit dialog.
+ function show_conflict_resolver(conflict) {
+ var updated = conflict.updated;
+ delete conflict.updated;
+ delete conflict.description_html;
+ var o = {
+ nsident: dup_model.get('nsident'),
+ summary: dup_model.get('summary'),
+ conflict: conflict,
+ updated: updated,
+ ABSWEB: ABSWEB
+ };
+ var CD = $(_.template(mtrack_underscore_templates['ticket-conflict'], o));
+ $('body').append(CD);
+ $('.timeinterval', CD).timeago();
+
+ // Fixup model so that we don't trigger a 409 on next save
+ // (unless there is a further conflict!)
+ dup_model.set({updated: o.updated});
+
+ dlg.modal('hide');
+ CD.modal('show');
+ CD.on('hidden', function() {
+ CD.remove();
+ });
+
+ $('button.mine', CD).click(function () {
+ var editor = new TE({
+ model: dup_model,
+ fields: view.options.fields
+ });
+ CD.modal('hide');
+ editor.show(on_success);
+ });
+
+ $('button.theirs', CD).click(function () {
+ var o = {};
+ for (var k in conflict) {
+ var item = conflict[k];
+ o[k] = item[1];
+ }
+ dup_model.set(o);
+
+ var editor = new TE({
+ model: dup_model,
+ fields: view.options.fields
+ });
+ CD.modal('hide');
+ editor.show(on_success);
+ });
+ }
+ /*
+ $('.modal-footer button.conflict', dlg).click(function () {
+ show_conflict_resolver({
+ updated: {
+ who: 'otherguy',
+ when: "2012-04-11T14:54:36+00:00",
+ cid: "17"
+ },
+ summary: ['my lemons', 'your lemons']
+ });
+ });
+ */
+
+ // Save. We want to apply the attributes from the dup_model to
+ // the real model.
+ $('.modal-footer button.btn-primary', dlg).click(function () {
+ view.model.save(dup_model.attributes, {
+ success: function(model) {
+ if ('success' in on_success) {
+ on_success.success(model);
+ }
+ dlg.modal('hide');
+ },
+ error: function (model, resp) {
+ // If a conflict was detected, show some useful UI to help
+ // them through it
+ var is_conflict = false;
+
+ if (_.isObject(resp)) {
+ try {
+ var r = JSON.parse(resp.responseText);
+ if (r.code == 409) {
+ is_conflict = r.extra;
+ }
+ } catch (e) {
+ }
+ }
+
+ if (!is_conflict) {
+ mtrack_ajax_error_to_dom(resp, $('div.modal-header', dlg));
+ return;
+ }
+
+ show_conflict_resolver(is_conflict);
+ }
+ });
+ });
+
+ dlg.modal('show');
+ }
+});
+
+// Ticket viewer
+var TV = Backbone.View.extend({
+ render: function() {
+ var t = _.template(mtrack_underscore_templates['ticket-show']);
+ var o = this.model.toJSON();
+ $(this.el).html(t(o));
+// $('body').attr('data-target', '#tkt-nav');
+// $('body').scrollspy({offset: 30});
+
+ var F = $('#tkt-fields');
+ // Table row template
+ var TR = F.find('tr');
+ var table = TR.parent();
+ TR.remove();
+
+ var model = this.model;
+ var view = this;
+
+ var attach_list = $('#attach-list', this.el);
+ function redraw_attachment_list() {
+ attach_list.empty();
+ var t = _.template(mtrack_underscore_templates['attachment-item']);
+ var count = 0;
+
+ model.getAttachments().each(function (att) {
+ count++;
+ var o = att.toJSON();
+ if ('width' in o) {
+ o.image = true;
+ } else {
+ o.image = false;
+ }
+ var d = $(t(o));
+ d.data('attachment-model', att);
+ attach_list.append(d);
+ });
+ if (count) {
+ $('.timeinterval', attach_list).timeago();
+ $('#attach', view.el).show();
+ $('.tkt-outline a[href=#attach]').show();
+ } else {
+ $('#attach', view.el).hide();
+ $('.tkt-outline a[href=#attach]').hide();
+ }
+ }
+ redraw_attachment_list();
+ model.getAttachments().bind('all', function () {
+ redraw_attachment_list();
+ });
+
+ var change_tpl = _.template(
+ mtrack_underscore_templates['ticket-event-show']);
+ var change_cont = $('#tkt-comments', this.el);
+
+ function add_one_change(cs) {
+ var d = $('<div/>');
+ $(d).html(change_tpl(cs.toJSON()));
+ var had_comment = false;
+ var commit = false;
+ _.each(cs.get('audit'), function (a) {
+ if (a.label == 'Comment') {
+ had_comment = true;
+ if (a.value.match(/^\(In /)) {
+ commit = true;
+ }
+ }
+ });
+ if (had_comment) {
+ d.addClass('chg-comment');
+ if (commit) {
+ d.addClass('chg-commit');
+ }
+ } else {
+ d.addClass('chg-no-comment');
+ }
+ d.appendTo(change_cont);
+ $('.toggle-desc', d).click(function () {
+ $('#' + $(this).attr('desc-id')).toggle();
+ return false;
+ });
+ $('.timeinterval', d).timeago();
+ }
+ function redraw_change_list() {
+ model.getChanges().each(function (cs) {
+ add_one_change(cs);
+ });
+ }
+ redraw_change_list();
+
+ function refresh_changes() {
+ var changes = model.getChanges();
+ var recent =
changes.at(0);
+
+ // If a modal dialog is open, don't do any updating
+ if ($('body').hasClass('modal-open')) {
return;
}
- if (ent.action == 'deleted') {
- %><b><%- ent.label %></b><%- ent.action %><%
- } else if (ent.label != 'Description') {
- if (_.isObject(ent.value)) {
- %>
- <b><%- ent.label %></b> →
- <%
- var cls = ent.label.toLowerCase();
- var url = null;
- if (cls == 'milestone') {
- url = ABSWEB + 'milestone.php/';
+ if (!recent) {
+ return;
+ }
+
+ changes.fetch({
+ success: function (c, r) {
+ if (!r.length) {
+ return;
}
- if (cls == 'keyword') {
- url = ABSWEB + 'search.php?q=keyword:';
+ var latest =
changes.at(0);
+ if (
latest.id ==
recent.id) {
+ // No change
+ return;
}
- if (cls == 'dependencies' || cls == 'blocks' ||
- cls == 'children' || cls == 'parent') {
- cls = 'ticketlink';
- url = ABSWEB + 'ticket.php/';
- }
- for (var id in ent.value) {
- if (url) {
- %><span class="<%- cls %>"><a href="<%- url %><%- ent.value[id] %>"><%- ent.value[id] %></a></span><%
- } else {
- %><span class="<%- cls %>"><%- ent.value[id] %></span><%
+ model.getAttachments().fetch();
+ var base_cid = model.get('updated').cid;
+ model.fetch({
+ success: function (model, resp) {
+ if (model.get('updated').cid == base_cid) {
+ return;
+ }
+ /* it changed */
+ model.unset('comment', {silent: true});
+ view.render();
}
- }
- } else {
- %>
- <b><%- ent.label %></b> → <%- ent.value %>
- <%
+ });
}
+ });
+ }
+ function disable_change_refresh() {
+ clearInterval(view.timer);
+ view.timer = null;
+ }
+ function enable_change_refresh() {
+ view.timer = setInterval(refresh_changes, 60000);
+ }
+ enable_change_refresh();
+
+ var user_template = _.template(mtrack_underscore_templates['user-name']);
+ function render_user(user) {
+ if (!_.isObject(user)) {
+ user = {
+ id: user,
+ label: user
+ };
+ }
+ var o = _.clone(user);
+ o.ABSWEB = ABSWEB;
+ return user_template(o);
+ }
+
+ function render_user_list(users) {
+ var res = [];
+ _.each(users, function(user) {
+ res.push(render_user(user));
+ });
+ return res.join(' ');
+ }
+
+ function render_change_time(item) {
+ item = _.clone(item);
+ item.ABSWEB = ABSWEB;
+ return _.template(mtrack_underscore_templates['item-changed'], item);
+ }
+
+ var renderers = {
+ 'owner': render_user,
+ 'cc': render_user_list,
+ 'created': render_change_time,
+ 'updated': render_change_time,
+ };
+
+ function process_field(field) {
+ if (
field.name == 'description') {
+ return;
+ }
+ var val = model.get(
field.name);
+ if (typeof(val) == 'undefined' || val == null) {
+ return;
+ }
+ if (_.isArray(val) && val.length == 0) {
+ return;
+ }
+ if (typeof(val) == 'string' && val == '') {
+ return;
+ }
+ var tr = TR.clone();
+ $('td.fieldname', tr).text(field.label).attr('title', field.label);
+ if (
field.name in renderers) {
+ $('td.fieldvalue', tr).html(renderers[
field.name](val));
} else {
- %>
- <b><%- ent.label %></b><%- ent.action %><button class="btn toggle-desc" desc-id="desc-<%- ent.cid %>">Toggle</button>
- <p id="desc-<%- ent.cid %>" class="hide-desc"><%- ent.value %></p>
- <%
+ // If we have a template defined, then use the generic template
+ // based renderer. First look to see if we have a template
+ // for the specific field name, then try to fall back to a
+ // generic formatter by type.
+ var tplname = "ticket-field-byname-" +
field.name;
+ if (!(tplname in mtrack_underscore_templates)) {
+ // Try by type
+ tplname = "ticket-field-bytype-" + field.type;
+ }
+
+ var render = function (a) {
+ return $('<div/>').text(a).html();
+ };
+
+ if (tplname in mtrack_underscore_templates) {
+ var t = _.template(mtrack_underscore_templates[tplname]);
+ render = function (a) {
+ /* wrap it up so that the value itself is accessible,
+ * as some of the data types we have encode the ids
+ * in the keys of an object and we can't iterate
+ * the context without a name in the template handler */
+ var o = {
+ value: a,
+ ABSWEB: ABSWEB
+ };
+ return t(o);
+ };
+ }
+ $('td.fieldvalue', tr).html(render(val));
}
- %>
+ table.append(tr);
+ }
- <br/>
- <%
+ if (model.get('status') == 'closed') {
+ process_field({name: 'resolution', label: 'Resolved'});
+ } else {
+ process_field({name: 'status', label: 'Status'});
+ }
+ if (model.get('parent')) {
+ process_field({name: 'parent', label: 'Parent', type: 'ticket'});
+ }
+
+ process_field({name: 'created', label: 'Opened'});
+ if (model.get('updated') &&
+ model.get('updated').cid != model.get('created').cid) {
+ process_field({name: 'updated', label: 'Updated'});
+ }
+
+ for (var gidx in this.options.fields) {
+ var group = this.options.fields[gidx];
+ _.each(group.fields, process_field);
+ }
+ $('.timeinterval', this.el).timeago();
+
+ $('#togglebtn', this.el).click(function () {
+ $('#tkt-fields', this.el).toggle();
+ return false;
});
- if (comment) { print(comment); }
- %>
- </div>
-</script>
-<script type='text/javascript'>
+ // Open the ticket editor
+ $('#editbtn', this.el).click(function () {
+ var editor = new TE({
+ model: view.model,
+ fields: view.options.fields
+ });
+ editor.show({
+ success: function (model) {
+ view.render();
+ },
+ hidden: function () {
+ refresh_changes();
+ }
+ });
+ return false;
+ });
+
+ /* operates on the result of a ticket split; if saved,
+ * we're taken to the ticket page for the newly saved ticket */
+ function edit_split(model) {
+ var editor = new TE({
+ model: model,
+ fields: view.options.fields
+ });
+ editor.show({
+ success: function (model) {
+ /* go to that ticket page */
+ window.location = ABSWEB + 'ticket.php/' + model.get('nsident');
+ },
+ hidden: function () {
+ refresh_changes();
+ }
+ });
+ return false;
+ }
+
+ function make_split_ticket() {
+ var S = view.model.clone();
+ console.log("cloned as", S);
+ S.unset('spent');
+ S.unset('remaining');
+ S.unset('estimated');
+ S.unset('nsident');
+ S.unset('id');
+ S.unset('created');
+ S.unset('updated');
+ S.set({children: []});
+ S.set({description:
+ "\\nSplit from #" + view.model.get('nsident') + "\\n\\n---\\n\\n" +
+ view.model.get('description')});
+ // Pointless to clone it in a closed state
+ if (S.get('status') == 'closed') {
+ S.set({status: 'open'});
+ }
+ S.unset('resolution');
+ return S;
+ }
+ $('#splitsib', this.el).click(function () {
+ edit_split(make_split_ticket());
+ return false;
+ });
+ $('#splitchild', this.el).click(function () {
+ var S = make_split_ticket();
+ // New ticket is a child of the current one
+ S.set({ptid:
view.model.id});
+ edit_split(S);
+ return false;
+ });
+
+ if (view.model.isNew()) {
+ var editor = new TE({
+ model: view.model,
+ fields: view.options.fields
+ });
+ editor.show({
+ success: function (model) {
+ window.location = ABSWEB + 'ticket.php/' + model.get('nsident');
+ },
+ hidden: function () {
+ refresh_changes();
+ }
+ });
+ }
+ return this;
+ }
+});
+
$(document).ready(function() {
var TheTicket = null;
var base_ticket = $TICKET;
@@ -225,326 +915,23 @@
var attachments = $ATTACH;
var comment_editor = null;
- function reset_editor() {
- if (!TheTicket) {
- TheTicket = new MTrackTicket(base_ticket);
- TheTicket.mtrack_edit_count = 0;
- TheTicket.getChanges().reset(changes);
- TheTicket.getAttachments().reset(attachments);
- } else {
- TheTicket.set(base_ticket);
- TheTicket.changed = false;
- }
- }
- reset_editor();
+ TheTicket = new MTrackTicket(base_ticket);
+ TheTicket.getAttachments().reset(attachments);
+ TheTicket.getChanges().reset(changes);
- editor = new MTrackMainTicketEditorView({
- model: TheTicket,
- readonly: !editable,
- fieldset: FIELDSET,
- el: '#tktedit'
+ TheTicket.bind('change:summary', function() {
+ $('html head title').text('#' + TheTicket.get('nsident') + ' ' +
+ TheTicket.get('summary'));
});
- var change_view = new MTrackTicketChangesView({
+ var V = new TV({
model: TheTicket,
- collection: TheTicket.getChanges(),
- el: '#issue-changes'
+ fields: FIELDSET,
+ el: $('#ticket')
});
-
- var attach_view = new MTrackTicketAttachmentsView({
- model: TheTicket,
- editable: editable,
- collection: TheTicket.getAttachments(),
- el: '#issue-attachments'
- });
-
- TheTicket.bind('change', function () {
- var id = TheTicket.get('nsident');
- if (id) {
- $('html head title').text('#' + id + ' ' + TheTicket.get('summary'));
- }
-
- if (TheTicket.mtrack_fetching) return;
- if (TheTicket.hasChanged()) {
- TheTicket.changed = true;
- }
- if (TheTicket.changed) {
- $('#issue-buttons button.hide-until-change').fadeIn('fast');
- }
- });
-
- window.onbeforeunload = function() {
- if (TheTicket.changed) {
- return "You haven't saved your changes!";
- }
- };
-
- TheTicket.bind('error', function (model, err) {
- $('#issue-error-text').text(err);
- $('#issue-error').fadeIn('fast')
- });
-
- comment_editor = new MTrackWikiTextAreaView({
- model: TheTicket,
- wikiContext: "ticket:",
- use_overlay: true,
- Caption: "Edit Comment text",
- OKLabel: "Add Comment",
- CancelLabel: "Abandon changes to comment",
- readonly: !editable,
- srcattr: "comment",
- el: "#commentedit"
- });
-
- function refresh_lists() {
- var Changes = TheTicket.getChanges();
- var recent = Changes.at(0);
- if (!recent) {
- return;
- }
- Changes.fetch({
- success: function (c, r) {
- if (r.length) {
- TheTicket.getAttachments().fetch({
- success: function () {
- attachments = TheTicket.getAttachments().models;
- }
- });
- if (!TheTicket.changed && TheTicket.mtrack_edit_count == 0) {
- TheTicket.mtrack_fetching = true;
- TheTicket.fetch({
- error: function() {
- TheTicket.mtrack_fetching = false;
- },
- success: function (model, resp) {
- TheTicket.mtrack_fetching = false;
- if (model.get('updated').cid != base_ticket.updated.cid) {
- /* it changed */
- base_ticket = TheTicket.toJSON();
- TheTicket.unset('comment', {silent: true});
- editor.render();
- }
- }
- });
- }
- }
- changes = Changes.models;
- },
- data: {
- last:
recent.id
- },
- add: true
- });
- }
-
- function check_for_changes() {
- refresh_lists();
- }
- if (!TheTicket.isNew()) {
- setInterval(check_for_changes, 60000);
- }
-
- $('#issue-error').click(function () {
- $(this).fadeOut('fast');
- });
-
- $('#save-issue').click(function () {
- $('#issue-error').fadeOut('fast');
- var overlay = $('<div class="overlay"/>');
- overlay.appendTo('body').fadeIn('fast', function () {
- TheTicket.save(TheTicket.toJSON(), {
- success: function(model, response) {
- $('#issue-buttons button.hide-until-change').fadeOut('fast');
- overlay.fadeOut('fast', function () {
- if (base_ticket.nsident == null) {
- /* we just saved the initial version; revise the URL
- * to reflect our new status */
- var url = ABSWEB + 'ticket.php/' + TheTicket.get('nsident');
- window.onbeforeunload = null;
- window.location.href = url;
- return;
- }
- base_ticket = TheTicket.toJSON();
- TheTicket.unset('comment', {silent: true});
- editor.render();
- refresh_lists();
- overlay.remove();
- TheTicket.changed = false;
- });
- },
- error: function(model, response) {
- var err;
- var conflict = null;
- if (!_.isObject(response)) {
- err = response;
- } else {
- err = response.statusText;
- try {
- var r = JSON.parse(response.responseText);
- err = r.message;
- if (r.code == 409) {
- conflict = r.extra;
- }
- } catch (e) {
- err = response.statusText;
- }
- }
- refresh_lists();
-
- if (conflict) {
- var tbl = $('#conflict-list');
- tbl.empty();
- TheTicket.set({updated: conflict.updated});
- delete conflict.updated;
- for (var k in conflict) {
- if (k == 'description_html') {
- continue;
- }
- var item = conflict[k];
- var o = {
- field: k,
- yours: item[0],
- theirs: item[1]
- };
- $(_.template(
- "<tr><td><%- field %></td><td><%- yours %></td><td><%- theirs %></td></tr>", o)).
- appendTo(tbl);
- }
- $('#conflict-form').fadeIn('fast');
- $('#conflict-keep').click(function () {
- $('#conflict-form').fadeOut('fast');
- overlay.fadeOut('fast', function () {
- overlay.remove();
- });
- return false;
- });
- $('#conflict-take').click(function () {
- $('#conflict-form').fadeOut('fast');
- var o = {};
- for (var k in conflict) {
- var item = conflict[k];
- o[k] = item[1];
- }
- TheTicket.set(o);
-
- overlay.fadeOut('fast', function () {
- overlay.remove();
- });
- return false;
- });
- } else {
- $('#issue-error-text').text(err);
- $('#issue-error').fadeIn('fast')
- overlay.fadeOut('fast', function () {
- overlay.remove();
- });
- }
- }
- });
- });
- });
-
- $('#cancel-issue').click(function () {
- $('#issue-error').fadeOut('fast');
- reset_editor();
- $('#issue-buttons button.hide-until-change').fadeOut('fast');
- });
-
- var in_reply = false;
- var orig_comment = null;
-
- if (editable) {
- comment_editor.bind('canceledit', function () {
- console.log("cancel");
- if (in_reply) {
- in_reply = false;
- TheTicket.set({comment: orig_comment},{silent:true});
- }
- });
- TheTicket.bind('change:comment', function () {
- in_reply = false;
- orig_comment = null;
- });
- $('#comment-issue').click(function () {
- comment_editor.edit();
- return false;
- });
- } else {
- $('#comment-issue').hide();
- }
-
- function reply_comment(cid) {
- var c = TheTicket.getChanges().get(cid);
- orig_comment = TheTicket.get("comment");
- var comment = orig_comment || '';
- if (comment.length) {
- comment = comment + "\\n\\n";
- }
- var reason = c.get('reason');
- // cite it
- reason = reason.replace(/^(\s*)/mg, "> \$1");
- comment = comment + "Replying to [comment:" + cid + " a comment by " +
- c.get('who') + "]\\n" + reason + "\\n";
- in_reply = true;
- TheTicket.set({'comment': comment}, {silent: true});
- comment_editor.edit();
- }
- window.mtrack_reply_comment = reply_comment;
-
-
- function calc_soff() {
- var b = $('#issue-buttons');
- var d = b.position().top - $(window).scrollTop() + b.height() + 10;
- return d;
- }
-
- var clicking = false;
- /* color the "tabs" based on the scroll position */
- function highlight_tab() {
- var soff = calc_soff();
- var y = $(window).scrollTop();
- var active = null;
- $('#issue-content a').each(function () {
- var what = $(this).attr('href');
- var target = $(what);
-
- var pos = $(target).position()['top'] - soff;
- $(this).removeClass('active');
- if (y >= pos) {
- active = $(this);
- }
- });
- if (active) {
- active.addClass('active');
- }
- }
- $(window).scroll(function() {
- if (!clicking) {
- highlight_tab();
- }
- });
-
- $('#issue-content a').click(function () {
- var what = $(this).attr('href');
- var target = $(what);
- var d = calc_soff();
- var t = target.offset().top - d;
- clicking = true;
- $('#issue-content a').removeClass('active');
- $(this).addClass('active');
- $('html, body').animate(
- {scrollTop: t},
- 350,
- 'easeOutQuint',
- function () {
- clicking = false;
- }
- );
- return false;
- });
+ V.render();
});
</script>
HTML;
mtrack_foot();
-
diff -r c063907c3b9411dd6a2718874351a29a9e5252aa -r 8c5ed4e4569ce7bef9a6b4b16c82350b6c3a47f4 web/ticket2.php
--- a/web/ticket2.php
+++ /dev/null
@@ -1,937 +0,0 @@
-<?php # vim:ts=2:sw=2:et:
-/* For licensing and copyright terms, see the file named LICENSE */
-include '../inc/common.php';
-
-if ($pi = mtrack_get_pathinfo()) {
- $id = $pi;
-} else {
- $id = $_GET['id'];
-}
-
-if ($id == 'new') {
- $issue = new MTrackIssue;
- $issue->priority = 'normal';
-} else {
- if (strlen($id) == 32) {
- $issue = MTrackIssue::loadById($id);
- } else {
- $issue = MTrackIssue::loadByNSIdent($id);
- }
- if (!$issue) {
- throw new Exception("Invalid ticket $id");
- }
-}
-
-$field_data = MTrackAPI::invoke('GET', '/ticket/meta/fields', null,
- array('tid' => $issue->tid))->result;
-
-$FIELDSET = json_encode($field_data);
-
-if ($id == 'new') {
- MTrackACL::requireAllRights("Tickets", 'create');
- $editable = 'true';
- mtrack_head("New ticket");
- $TICKET = json_encode(MTrackIssue::rest_return_ticket($issue));
- $CHANGES = json_encode(array());
- $ATTACH = json_encode(array());
-} else {
- MTrackACL::requireAllRights("ticket:" . $issue->tid, 'read');
- $editable = json_encode(
- MTrackACL::hasAllRights("ticket:" . $issue->tid, 'modify'));
- if ($issue->nsident) {
- mtrack_head("#$issue->nsident " . $issue->summary);
- } else {
- mtrack_head("#$id " . $issue->summary);
- }
- $TICKET = json_encode(MTrackAPI::invoke('GET', "/ticket/$id")->result);
- $CHANGES = json_encode(MTrackAPI::invoke(
- 'GET', "/ticket/$id/changes")->result);
- $ATTACH = json_encode(MTrackAPI::invoke(
- 'GET', "/ticket/$id/attach")->result);
-}
-
-echo <<<HTML
-<div id="ticket"></div>
-<script type='text/javascript'>
-// Ticket editor
-var TE = Backbone.View.extend({
- show: function(on_success) {
- var o = this.model.toJSON();
- o.isnew = this.model.isNew();
- // A clone of the model to use for editing with the existing
- // set of editors
- var dup_model = this.model.clone();
- dup_model.getAttachments().reset(this.model.getAttachments().models);
-
- $(this.el).html(_.template(
- mtrack_underscore_templates['ticket-edit'], o));
-
- var view = this;
- $(view.el).appendTo('body');
- var dlg = $('div.modal', view.el);
- var in_wiki = false;
-
- dlg.on('hidden', function () {
- if (!in_wiki) {
- $(view.el).remove();
- if ('hidden' in on_success) {
- on_success.hidden();
- }
- }
- });
-
- // Validation errors
- dup_model.bind('error', function (model, err) {
- mtrack_ajax_error_to_dom(err, $('div.modal-header', view.el));
- });
-
- var attach_list = $('#attach-list', dlg);
- attach_list.on('click', 'button.delattach', function() {
- var att = $(this).closest('div.attachment').data('attachment-model');
- var m = $("<div class='modal fade'><div class='modal-header'><a class='close' data-dismiss='modal'>x</a><h3>Delete Attachment?</h3></div><div class='modal-body'><p><b></b></p><p>Do you really want to delete this attachment?</p><p>You cannot undo this action!</p></div><div class='modal-footer'><button class='btn' data-dismiss='modal'>Close</button><button class='btn btn-danger'>Delete</button></div></div>");
-
- $('b', m).text(att.get('filename'));
- $('.btn-danger', m).click(function () {
- att.destroy();
- m.modal('hide');
- });
-
- m.on('hidden', function() {
- m.remove();
- });
- m.modal();
-
- });
- function redraw_attachment_list() {
- attach_list.empty();
- var t = _.template(mtrack_underscore_templates['attachment-item-edit']);
-
- dup_model.getAttachments().each(function (att) {
- var o = att.toJSON();
- if ('width' in o) {
- o.image = true;
- } else {
- o.image = false;
- }
- var d = $(t(o));
- d.data('attachment-model', att);
- attach_list.append(d);
- });
- $('.timeinterval', attach_list).timeago();
- }
- redraw_attachment_list();
- dup_model.getAttachments().bind('all', function () {
- redraw_attachment_list();
- });
-
- // Grab template elements and take them out of the DOM
- var tab_ul = $('ul.nav-tabs', view.el);
- var tab_hdr = $('li:first', tab_ul);
- tab_hdr.remove();
- var tab_att = $('li', tab_ul);
- tab_att.remove(); // we'll append it at the end
- var tab_content = $('div.tab-content', view.el);
- var tab_body = $('div.tab-pane:first', tab_content);
- tab_body.remove();
-
- var next_tab_id = 1;
- function add_group_tab(group) {
- var label;
- if (
group.name == 0) {
- label = 'Details';
- } else {
- label =
group.name;
- }
-
- var id = 'tab-' + next_tab_id++;
-
- var hdr = tab_hdr.clone();
- $('a', hdr).attr('href', '#' + id);
- $('a', hdr).text(label);
- tab_ul.append(hdr);
-
- var tab = tab_body.clone();
- tab.attr('id', id);
- tab_content.append(tab);
-
- if (next_tab_id == 2) {
- // Make the first one active
- $('a', hdr).trigger('click');
- }
-
- return tab;
- }
-
- function multi_editor(lcont, tab, field) {
- var label = $('<label/>');
- label.text(field.label);
- tab.append(label);
-
- var edit = $('<textarea/>', {
- cols: field.cols,
- rows: field.rows,
- placeholder: field.placeholder
- });
- var val = dup_model.get(
field.name);
- if (val) {
- edit.val(val);
- }
- edit.on('change', function() {
- var o = {};
- o[
field.name] = edit.val();
- dup_model.set(o);
- });
- lcont.remove();
- tab.append(edit);
- tab.attr('colspan', 2);
- }
-
- function wiki_editor(lcont, tab, field) {
- var label = $('<label/>');
- label.text(field.label);
- tab.append(label);
-
- var edit = $('<textarea/>', {
- class: 'wiki shortwiki',
- cols: field.cols,
- rows: field.rows,
- placeholder: field.placeholder
- });
- var val = dup_model.get(
field.name);
- if (val) {
- edit.val(val);
- }
- lcont.remove();
- tab.append(edit);
- var b = $('<button/>', {
- class: 'btn'
- });
- b.html('<i class="icon-pencil"></i> Edit ' + field.label + ' in wiki editor');
- tab.append(b);
- tab.attr('colspan', 2);
-
- dup_model.bind('change:' +
field.name, function () {
- var val = dup_model.get(
field.name);
- edit.val(val ? val : '');
- });
- edit.on('change', function() {
- var o = {};
- o[
field.name] = edit.val();
- dup_model.set(o);
- });
-
- var wiki = new MTrackWikiTextAreaView({
- model: dup_model,
- wikiContext: 'ticket:',
- use_overlay: true,
- Caption: "Edit " + field.label,
- OKLabel: "Accept " + field.label,
- CancelLabel: "Abandon changes to " + field.label,
- srcattr:
field.name,
- renderedattr:
field.name + '_html'
- });
- wiki.bind('editstart', function () {
- in_wiki = true;
- setTimeout(function () {
- dlg.modal('hide');
- }, 1000);
- });
- wiki.bind('editend', function () {
- in_wiki = false;
- dlg.modal('show');
- });
-
- b.click(function () {
- var o = {};
- o[
field.name] = edit.val();
- dup_model.set(o);
- wiki.edit();
- });
- }
-
- function text_editor(lcont, tab, field) {
- lcont.text(field.label);
-
- var inp = $('<input/>', {
- type: "text",
- name:
field.name,
- placeholder: field.placeholder
- });
- var val = view.model.get(
field.name);
- if (val) {
- inp.val(val);
- }
- inp.on('change', function() {
- var o = {};
- o[
field.name] = inp.val();
- dup_model.set(o);
- });
-
- tab.append(inp);
- }
-
- function multiselect_editor(lcont, tab, field) {
- lcont.text(field.label);
-
- var el = $('<div/>');
- tab.append(el);
- var view = new MTrackSelectEditorView({
- el: el,
- model: dup_model,
- multiple: true,
- srcattr:
field.name,
- label: field.label,
- width: '418px',
- values: field.options,
- defval: field["default"],
- placeholder: field.placeholder
- });
- view.render();
- }
-
- function select_editor(lcont, tab, field) {
- lcont.text(field.label);
-
- var el = $('<span/>');
- tab.append(el);
- var view = new MTrackSelectEditorView({
- el: el,
- model: dup_model,
- srcattr:
field.name,
- label: field.label,
- width: '418px',
- values: field.options,
- defval: field["default"],
- placeholder: field.placeholder
- });
- view.render();
- }
-
- function ticketdeps_editor(lcont, tab, field) {
- lcont.text(field.label);
-
- var el = $('<span/>');
- tab.append(el);
- var view = new MTrackTicketDepEditView({
- el: el,
- model: dup_model,
- srcattr:
field.name,
- label: field.label,
- });
- view.render();
- }
-
- function tags_editor(lcont, tab, field) {
- lcont.text(field.label);
-
- var el = $('<span/>');
- tab.append(el);
- var view = new MTrackTagEditView({
- el: el,
- model: dup_model,
- srcattr:
field.name,
- label: field.label,
- });
- view.render();
- }
-
- function cc_editor(lcont, tab, field) {
- lcont.text(field.label);
-
- var el = $('<span/>');
- tab.append(el);
- var view = new MTrackCcEditView({
- el: el,
- model: dup_model,
- srcattr:
field.name,
- label: field.label,
- });
- view.render();
- }
-
- var editors = {
- multi: multi_editor,
- select: select_editor,
- multiselect: multiselect_editor,
- tags: tags_editor,
- cc: cc_editor,
- ticketdeps: ticketdeps_editor,
- text: text_editor,
- wiki: wiki_editor
- };
-
- function add_editor(tr, tab, field)
- {
- if (field.type == 'readonly') return;
-
- var editor = text_editor;
-
- if (field.type in editors) {
- editor = editors[field.type];
- }
- tr = tr.clone();
- var lcont = $('td.fieldname', tr);
- var div = $('td.fieldvalue', tr);
- tab.append(tr);
- editor(lcont, div, field);
- }
-
- function add_tab_and_fields(group) {
- var tab = add_group_tab(group);
-
- var table = $('table', tab);
- var tr = $('tr', table);
- tr.remove();
-
- if (
group.name == 0) {
- // Synthesize some fields
- add_editor(tr, table, {
- name: 'summary',
- label: 'Summary'
- });
- add_editor(tr, table, {
- name: 'status',
- label: 'Status',
- type: 'select',
- options: mtrack_ticket_states
- });
- if (dup_model.get('status') != 'closed') {
- add_editor(tr, table, {
- name: 'resolution',
- label: 'Resolution',
- placeholder: 'Resolve ticket as...',
- type: 'select',
- options: mtrack_resolutions
- });
- }
- }
- _.each(group.fields, function (field) {
- add_editor(tr, table, field);
- });
- }
-
- var com_tab = add_tab_and_fields({
- name: 'Comment',
- fields: [
- {
- name: 'comment',
- label: 'Comment',
- type: 'wiki',
- placeholder: 'Something on your mind? Share it here!',
- rows: 10,
- cols: 78
- }
- ]
- });
-
- /* Create a tab for each category of field */
- for (var gidx in this.options.fields) {
- add_tab_and_fields(this.options.fields[gidx]);
- }
- tab_ul.append(tab_att);
-
- /* handle uploads */
- var uploading = false;
- $('#confirm-upload', dlg).click(function () {
- uploading = true;
- $('#upload-form', dlg).submit();
- });
- $('#upload_target', dlg).on('load', function () {
- var res = $(this).contents().find('body').text();
- try {
- res = JSON.parse(res);
- if (res.status == 'success') {
- if (uploading) {
- $('<div class="alert alert-success">' +
- '<a class="close" data-dismiss="alert">×</a>' +
- 'Upload successful</div>').
- appendTo($('#tkt-edit-attachments', dlg));
- dup_model.getAttachments().reset(res.attachments);
- }
- $('input[type=file]', dlg).val('');
- } else {
- $('<div class="alert alert-danger">' +
- '<a class="close" data-dismiss="alert">×</a>' +
- res.message + '</div>').
- appendTo($('#tkt-edit-attachments', dlg));
- }
- } catch (e) {
- }
- uploading = false;
- });
-
- // Present the conflict resolution UI.
- // This is an alternative form that shows the changes side-by-side
- // and allows the user to pick a resolution:
- // - Accept my changes
- // - Accept their changes
- // - Cancel
- // The first two will re-display the edit dialog with the updated
- // model, the latter will cancel the edit dialog.
- function show_conflict_resolver(conflict) {
- var updated = conflict.updated;
- delete conflict.updated;
- delete conflict.description_html;
- var o = {
- nsident: dup_model.get('nsident'),
- summary: dup_model.get('summary'),
- conflict: conflict,
- updated: updated,
- ABSWEB: ABSWEB
- };
- var CD = $(_.template(mtrack_underscore_templates['ticket-conflict'], o));
- $('body').append(CD);
- $('.timeinterval', CD).timeago();
-
- // Fixup model so that we don't trigger a 409 on next save
- // (unless there is a further conflict!)
- dup_model.set({updated: o.updated});
-
- dlg.modal('hide');
- CD.modal('show');
- CD.on('hidden', function() {
- CD.remove();
- });
-
- $('button.mine', CD).click(function () {
- var editor = new TE({
- model: dup_model,
- fields: view.options.fields
- });
- CD.modal('hide');
- editor.show(on_success);
- });
-
- $('button.theirs', CD).click(function () {
- var o = {};
- for (var k in conflict) {
- var item = conflict[k];
- o[k] = item[1];
- }
- dup_model.set(o);
-
- var editor = new TE({
- model: dup_model,
- fields: view.options.fields
- });
- CD.modal('hide');
- editor.show(on_success);
- });
- }
- /*
- $('.modal-footer button.conflict', dlg).click(function () {
- show_conflict_resolver({
- updated: {
- who: 'otherguy',
- when: "2012-04-11T14:54:36+00:00",
- cid: "17"
- },
- summary: ['my lemons', 'your lemons']
- });
- });
- */
-
- // Save. We want to apply the attributes from the dup_model to
- // the real model.
- $('.modal-footer button.btn-primary', dlg).click(function () {
- view.model.save(dup_model.attributes, {
- success: function(model) {
- if ('success' in on_success) {
- on_success.success(model);
- }
- dlg.modal('hide');
- },
- error: function (model, resp) {
- // If a conflict was detected, show some useful UI to help
- // them through it
- var is_conflict = false;
-
- if (_.isObject(resp)) {
- try {
- var r = JSON.parse(resp.responseText);
- if (r.code == 409) {
- is_conflict = r.extra;
- }
- } catch (e) {
- }
- }
-
- if (!is_conflict) {
- mtrack_ajax_error_to_dom(resp, $('div.modal-header', dlg));
- return;
- }
-
- show_conflict_resolver(is_conflict);
- }
- });
- });
-
- dlg.modal('show');
- }
-});
-
-// Ticket viewer
-var TV = Backbone.View.extend({
- render: function() {
- var t = _.template(mtrack_underscore_templates['ticket-show']);
- var o = this.model.toJSON();
- $(this.el).html(t(o));
-// $('body').attr('data-target', '#tkt-nav');
-// $('body').scrollspy({offset: 30});
-
- var F = $('#tkt-fields');
- // Table row template
- var TR = F.find('tr');
- var table = TR.parent();
- TR.remove();
-
- var model = this.model;
- var view = this;
-
- var attach_list = $('#attach-list', this.el);
- function redraw_attachment_list() {
- attach_list.empty();
- var t = _.template(mtrack_underscore_templates['attachment-item']);
- var count = 0;
-
- model.getAttachments().each(function (att) {
- count++;
- var o = att.toJSON();
- if ('width' in o) {
- o.image = true;
- } else {
- o.image = false;
- }
- var d = $(t(o));
- d.data('attachment-model', att);
- attach_list.append(d);
- });
- if (count) {
- $('.timeinterval', attach_list).timeago();
- $('#attach', view.el).show();
- $('.tkt-outline a[href=#attach]').show();
- } else {
- $('#attach', view.el).hide();
- $('.tkt-outline a[href=#attach]').hide();
- }
- }
- redraw_attachment_list();
- model.getAttachments().bind('all', function () {
- redraw_attachment_list();
- });
-
- var change_tpl = _.template(
- mtrack_underscore_templates['ticket-event-show']);
- var change_cont = $('#tkt-comments', this.el);
-
- function add_one_change(cs) {
- var d = $('<div/>');
- $(d).html(change_tpl(cs.toJSON()));
- var had_comment = false;
- var commit = false;
- _.each(cs.get('audit'), function (a) {
- if (a.label == 'Comment') {
- had_comment = true;
- if (a.value.match(/^\(In /)) {
- commit = true;
- }
- }
- });
- if (had_comment) {
- d.addClass('chg-comment');
- if (commit) {
- d.addClass('chg-commit');
- }
- } else {
- d.addClass('chg-no-comment');
- }
- d.appendTo(change_cont);
- $('.toggle-desc', d).click(function () {
- $('#' + $(this).attr('desc-id')).toggle();
- return false;
- });
- $('.timeinterval', d).timeago();
- }
- function redraw_change_list() {
- model.getChanges().each(function (cs) {
- add_one_change(cs);
- });
- }
- redraw_change_list();
-
- function refresh_changes() {
- var changes = model.getChanges();
- var recent =
changes.at(0);
-
- // If a modal dialog is open, don't do any updating
- if ($('body').hasClass('modal-open')) {
- return;
- }
-
- if (!recent) {
- return;
- }
-
- changes.fetch({
- success: function (c, r) {
- if (!r.length) {
- return;
- }
- var latest =
changes.at(0);
- if (
latest.id ==
recent.id) {
- // No change
- return;
- }
- model.getAttachments().fetch();
- var base_cid = model.get('updated').cid;
- model.fetch({
- success: function (model, resp) {
- if (model.get('updated').cid == base_cid) {
- return;
- }
- /* it changed */
- model.unset('comment', {silent: true});
- view.render();
- }
- });
- }
- });
- }
- function disable_change_refresh() {
- clearInterval(view.timer);
- view.timer = null;
- }
- function enable_change_refresh() {
- view.timer = setInterval(refresh_changes, 60000);
- }
- enable_change_refresh();
-
- var user_template = _.template(mtrack_underscore_templates['user-name']);
- function render_user(user) {
- if (!_.isObject(user)) {
- user = {
- id: user,
- label: user
- };
- }
- var o = _.clone(user);
- o.ABSWEB = ABSWEB;
- return user_template(o);
- }
-
- function render_user_list(users) {
- var res = [];
- _.each(users, function(user) {
- res.push(render_user(user));
- });
- return res.join(' ');
- }
-
- function render_change_time(item) {
- item = _.clone(item);
- item.ABSWEB = ABSWEB;
- return _.template(mtrack_underscore_templates['item-changed'], item);
- }
-
- var renderers = {
- 'owner': render_user,
- 'cc': render_user_list,
- 'created': render_change_time,
- 'updated': render_change_time,
- };
-
- function process_field(field) {
- if (
field.name == 'description') {
- return;
- }
- var val = model.get(
field.name);
- if (typeof(val) == 'undefined' || val == null) {
- return;
- }
- if (_.isArray(val) && val.length == 0) {
- return;
- }
- if (typeof(val) == 'string' && val == '') {
- return;
- }
- var tr = TR.clone();
- $('td.fieldname', tr).text(field.label).attr('title', field.label);
- if (
field.name in renderers) {
- $('td.fieldvalue', tr).html(renderers[
field.name](val));
- } else {
- // If we have a template defined, then use the generic template
- // based renderer. First look to see if we have a template
- // for the specific field name, then try to fall back to a
- // generic formatter by type.
- var tplname = "ticket-field-byname-" +
field.name;
- if (!(tplname in mtrack_underscore_templates)) {
- // Try by type
- tplname = "ticket-field-bytype-" + field.type;
- }
-
- var render = function (a) {
- return $('<div/>').text(a).html();
- };
-
- if (tplname in mtrack_underscore_templates) {
- var t = _.template(mtrack_underscore_templates[tplname]);
- render = function (a) {
- /* wrap it up so that the value itself is accessible,
- * as some of the data types we have encode the ids
- * in the keys of an object and we can't iterate
- * the context without a name in the template handler */
- var o = {
- value: a,
- ABSWEB: ABSWEB
- };
- return t(o);
- };
- }
- $('td.fieldvalue', tr).html(render(val));
- }
- table.append(tr);
- }
-
- if (model.get('status') == 'closed') {
- process_field({name: 'resolution', label: 'Resolved'});
- } else {
- process_field({name: 'status', label: 'Status'});
- }
- if (model.get('parent')) {
- process_field({name: 'parent', label: 'Parent', type: 'ticket'});
- }
-
- process_field({name: 'created', label: 'Opened'});
- if (model.get('updated') &&
- model.get('updated').cid != model.get('created').cid) {
- process_field({name: 'updated', label: 'Updated'});
- }
-
- for (var gidx in this.options.fields) {
- var group = this.options.fields[gidx];
- _.each(group.fields, process_field);
- }
- $('.timeinterval', this.el).timeago();
-
- $('#togglebtn', this.el).click(function () {
- $('#tkt-fields', this.el).toggle();
- return false;
- });
-
- // Open the ticket editor
- $('#editbtn', this.el).click(function () {
- var editor = new TE({
- model: view.model,
- fields: view.options.fields
- });
- editor.show({
- success: function (model) {
- view.render();
- },
- hidden: function () {
- refresh_changes();
- }
- });
- return false;
- });
-
- /* operates on the result of a ticket split; if saved,
- * we're taken to the ticket page for the newly saved ticket */
- function edit_split(model) {
- var editor = new TE({
- model: model,
- fields: view.options.fields
- });
- editor.show({
- success: function (model) {
- /* go to that ticket page */
- window.location = ABSWEB + 'ticket.php/' + model.get('nsident');
- },
- hidden: function () {
- refresh_changes();
- }
- });
- return false;
- }
-
- function make_split_ticket() {
- var S = view.model.clone();
- console.log("cloned as", S);
- S.unset('spent');
- S.unset('remaining');
- S.unset('estimated');
- S.unset('nsident');
- S.unset('id');
- S.unset('created');
- S.unset('updated');
- S.set({children: []});
- S.set({description:
- "\\nSplit from #" + view.model.get('nsident') + "\\n\\n---\\n\\n" +
- view.model.get('description')});
- // Pointless to clone it in a closed state
- if (S.get('status') == 'closed') {
- S.set({status: 'open'});
- }
- S.unset('resolution');
- return S;
- }
- $('#splitsib', this.el).click(function () {
- edit_split(make_split_ticket());
- return false;
- });
- $('#splitchild', this.el).click(function () {
- var S = make_split_ticket();
- // New ticket is a child of the current one
- S.set({ptid:
view.model.id});
- edit_split(S);
- return false;
- });
-
- if (view.model.isNew()) {
- var editor = new TE({
- model: view.model,
- fields: view.options.fields
- });
- editor.show({
- success: function (model) {
- window.location = ABSWEB + 'ticket.php/' + model.get('nsident');
- },
- hidden: function () {
- refresh_changes();
- }
- });
- }
- return this;
- }
-});
-
-$(document).ready(function() {
- var TheTicket = null;
- var base_ticket = $TICKET;
- var FIELDSET = $FIELDSET;
- var editable = $editable;
- var editor = null;
- var changes = $CHANGES;
- var attachments = $ATTACH;
- var comment_editor = null;
-
- TheTicket = new MTrackTicket(base_ticket);
- TheTicket.getAttachments().reset(attachments);
- TheTicket.getChanges().reset(changes);
-
- TheTicket.bind('change:summary', function() {
- $('html head title').text('#' + TheTicket.get('nsident') + ' ' +
- TheTicket.get('summary'));
- });
-
- var V = new TV({
- model: TheTicket,
- fields: FIELDSET,
- el: $('#ticket')
- });
- V.render();
-});
-</script>
-HTML;
-
-mtrack_foot();
diff -r c063907c3b9411dd6a2718874351a29a9e5252aa -r 8c5ed4e4569ce7bef9a6b4b16c82350b6c3a47f4 web/ticketold.php
--- /dev/null
+++ b/web/ticketold.php
@@ -0,0 +1,550 @@
+<?php # vim:ts=2:sw=2:et:
+/* For licensing and copyright terms, see the file named LICENSE */
+include '../inc/common.php';
+
+if ($pi = mtrack_get_pathinfo()) {
+ $id = $pi;
+} else {
+ $id = $_GET['id'];
+}
+
+if ($id == 'new') {
+ $issue = new MTrackIssue;
+ $issue->priority = 'normal';
+} else {
+ if (strlen($id) == 32) {
+ $issue = MTrackIssue::loadById($id);
+ } else {
+ $issue = MTrackIssue::loadByNSIdent($id);
+ }
+ if (!$issue) {
+ throw new Exception("Invalid ticket $id");
+ }
+}
+
+$field_data = MTrackAPI::invoke('GET', '/ticket/meta/fields', null,
+ array('tid' => $issue->tid))->result;
+
+$FIELDSET = json_encode($field_data);
+
+if ($id == 'new') {
+ MTrackACL::requireAllRights("Tickets", 'create');
+ $editable = 'true';
+ mtrack_head("New ticket");
+ $TICKET = json_encode(MTrackIssue::rest_return_ticket($issue));
+ $CHANGES = json_encode(array());
+ $ATTACH = json_encode(array());
+} else {
+ MTrackACL::requireAllRights("ticket:" . $issue->tid, 'read');
+ $editable = json_encode(
+ MTrackACL::hasAllRights("ticket:" . $issue->tid, 'modify'));
+ if ($issue->nsident) {
+ mtrack_head("#$issue->nsident " . $issue->summary);
+ } else {
+ mtrack_head("#$id " . $issue->summary);
+ }
+ $TICKET = json_encode(MTrackAPI::invoke('GET', "/ticket/$id")->result);
+ $CHANGES = json_encode(MTrackAPI::invoke(
+ 'GET', "/ticket/$id/changes")->result);
+ $ATTACH = json_encode(MTrackAPI::invoke(
+ 'GET', "/ticket/$id/attach")->result);
+}
+
+echo <<<HTML
+<div id="attachment-form" class="popupForm" style="display:none">
+ <form action="${ABSWEB}post-attachment.php" method="POST"
+ id="upload-form" enctype="multipart/form-data" target="upload_target">
+ <input type="hidden" name="object" value="ticket:X">
+ <label for='attachments[]'>Select file(s) to be attached</label>
+ <input name="attachments[]" class='btn multi' type="file">
+ <iframe id="upload_target" name="upload_target" src="${ABSWEB}/mtrack.css">
+ </iframe>
+ <input type="submit" class='btn btn-primary' id="confirm-upload" value="Upload">
+ <button class='btn' id="cancel-upload">Cancel</button>
+ </form>
+</div>
+<div id="conflict-form" class="popupForm" style="display:none" -->
+ <h1>Conflicting changes</h1>
+ <p>Someone else has modified this ticket since you loaded the page.
+ The differences between your desired version of the ticket and the
+ currently saved version of the ticket are shown in the table below.
+ </p>
+ <br>
+ <table>
+ <thead>
+ <tr>
+ <th>Field</th><th>Yours</th><th>Theirs</th>
+ </tr>
+ </thead>
+ <tbody id="conflict-list"></tbody>
+ </table>
+ <br>
+ <p>
+ Click one of the buttons below; you will be returned to the editor
+ where you can make further changes (or cancel your changes).
+ When you next click the save button, your changes will resolve this
+ conflict and be applied to the ticket.
+ </p>
+ <button class='btn' id="conflict-keep">Keep my changes and return to editor</button>
+ <button class='btn' id="conflict-take">Take their changes and return to editor</button>
+</div>
+<div id="issue-buttons">
+ <div id="issue-content">
+ <ul>
+ <li><a href="#issue-container" class='active'>Description</a></li>
+ <li><a href="#attach">Attachments</a></li>
+ <li><a href="#change">Changes</a></li>
+ </ul>
+ </div>
+ <div id="issue-controls">
+ <div class='ui-state-error ui-corner-all' id='issue-error'>
+ <span class='ui-icon ui-icon-alert'></span>
+ <span id="issue-error-text"></span>
+ </div>
+ <button id="save-issue" class="btn btn-success hide-until-change">Save</button>
+ <button id="cancel-issue" class="btn hide-until-change">Cancel</button>
+ <button id="comment-issue" class='btn'><i class='icon-comment'></i> Comment</button>
+ </div>
+</div>
+<div id="issue-container">
+<div id='commentedit' class='popupForm' style='display:none'></div>
+<div id='tktedit'></div>
+<div id="issue-desc"></div>
+ <h2 id="attach">Attachments</h2>
+ <div id="issue-attachments"></div>
+ <h2 id="change">Changes</h2>
+ <div id="issue-changes"></div>
+</div>
+<div id="issue-props"></div>
+
+<script type="text/template" id='attach-template'>
+ <a class='attachment' href='<%= ABSWEB %>attachment.php/<%- object %>/<%- cid %>/<%- filename %>'><%- filename %></a> (<%- size %>) added by <%- who %>
+ <abbr class='timeinterval' title='<%- changedate %>'><%- changedate %></abbr>
+ <button class='btn btn-mini'><i class='icon-trash'></i></button>
+ <% if (image) {
+ var w = parseInt(width);
+ var h = parseInt(height);
+ var mw = 500;
+ if (w > mw) {
+ var s = w / mw;
+ height = h / s;
+ width = mw;
+ }
+ %>
+ <br><a href='<%= ABSWEB %>attachment.php/<%- object %>/<%- cid %>/<%- filename %>'><img src='<%= ABSWEB %>attachment.php/<%- object %>/<%- cid %>/<%- filename %>' width='<%- width %>' height='<%- height %>' border='0'></a>
+ <% } %>
+</script>
+
+<script type="text/template" id='ticket-edit-template'>
+ <h1><% if (status == 'closed') { %><del><% } %>
+ <% if (nsident) { %>
+ #<%- nsident %>
+ <% } else { %>
+ [NEW]
+ <% } %><span id="tkt-summary-text"></span>
+ <% if (status == 'closed') { %></del><% } %>
+ </h1>
+ <button class='btn' id='edit-description'>Edit Description</button>
+</script>
+
+<script type="text/template" id='ticket-change-template'>
+ <div class='ticketevent'>
+ <a class='pmark' href='#<%- id %>'>#</a><a name='<%- id %>'> </a><abbr class='timeinterval' title='<%- changedate %>'><%- changedate %></abbr><%- who %>
+ <a class='replycomment' href="javascript:mtrack_reply_comment(<%- id %>);">reply</a>
+ </div>
+ <div class='ticketchangeinfo'>
+ <img class='gravatar' src="${ABSWEB}avatar.php?u=<%- who %>&s=48">
+ <%
+ var comment = null;
+
+ _.each(audit, function (ent) {
+ if (ent.label == 'Nsident') {
+ return;
+ }
+ if (ent.label == 'Comment') {
+ comment = ent.value_html;
+ return;
+ }
+
+ if (ent.action == 'deleted') {
+ %><b><%- ent.label %></b><%- ent.action %><%
+ } else if (ent.label != 'Description') {
+ if (_.isObject(ent.value)) {
+ %>
+ <b><%- ent.label %></b> →
+ <%
+ var cls = ent.label.toLowerCase();
+ var url = null;
+ if (cls == 'milestone') {
+ url = ABSWEB + 'milestone.php/';
+ }
+ if (cls == 'keyword') {
+ url = ABSWEB + 'search.php?q=keyword:';
+ }
+ if (cls == 'dependencies' || cls == 'blocks' ||
+ cls == 'children' || cls == 'parent') {
+ cls = 'ticketlink';
+ url = ABSWEB + 'ticket.php/';
+ }
+ for (var id in ent.value) {
+ if (url) {
+ %><span class="<%- cls %>"><a href="<%- url %><%- ent.value[id] %>"><%- ent.value[id] %></a></span><%
+ } else {
+ %><span class="<%- cls %>"><%- ent.value[id] %></span><%
+ }
+ }
+ } else {
+ %>
+ <b><%- ent.label %></b> → <%- ent.value %>
+ <%
+ }
+ } else {
+ %>
+ <b><%- ent.label %></b><%- ent.action %><button class="btn toggle-desc" desc-id="desc-<%- ent.cid %>">Toggle</button>
+ <p id="desc-<%- ent.cid %>" class="hide-desc"><%- ent.value %></p>
+ <%
+ }
+ %>
+
+ <br/>
+ <%
+ });
+ if (comment) { print(comment); }
+ %>
+ </div>
+</script>
+
+<script type='text/javascript'>
+$(document).ready(function() {
+ var TheTicket = null;
+ var base_ticket = $TICKET;
+ var FIELDSET = $FIELDSET;
+ var editable = $editable;
+ var editor = null;
+ var changes = $CHANGES;
+ var attachments = $ATTACH;
+ var comment_editor = null;
+
+ function reset_editor() {
+ if (!TheTicket) {
+ TheTicket = new MTrackTicket(base_ticket);
+ TheTicket.mtrack_edit_count = 0;
+ TheTicket.getChanges().reset(changes);
+ TheTicket.getAttachments().reset(attachments);
+ } else {
+ TheTicket.set(base_ticket);
+ TheTicket.changed = false;
+ }
+ }
+ reset_editor();
+
+ editor = new MTrackMainTicketEditorView({
+ model: TheTicket,
+ readonly: !editable,
+ fieldset: FIELDSET,
+ el: '#tktedit'
+ });
+
+ var change_view = new MTrackTicketChangesView({
+ model: TheTicket,
+ collection: TheTicket.getChanges(),
+ el: '#issue-changes'
+ });
+
+ var attach_view = new MTrackTicketAttachmentsView({
+ model: TheTicket,
+ editable: editable,
+ collection: TheTicket.getAttachments(),
+ el: '#issue-attachments'
+ });
+
+ TheTicket.bind('change', function () {
+ var id = TheTicket.get('nsident');
+ if (id) {
+ $('html head title').text('#' + id + ' ' + TheTicket.get('summary'));
+ }
+
+ if (TheTicket.mtrack_fetching) return;
+ if (TheTicket.hasChanged()) {
+ TheTicket.changed = true;
+ }
+ if (TheTicket.changed) {
+ $('#issue-buttons button.hide-until-change').fadeIn('fast');
+ }
+ });
+
+ window.onbeforeunload = function() {
+ if (TheTicket.changed) {
+ return "You haven't saved your changes!";
+ }
+ };
+
+ TheTicket.bind('error', function (model, err) {
+ $('#issue-error-text').text(err);
+ $('#issue-error').fadeIn('fast')
+ });
+
+ comment_editor = new MTrackWikiTextAreaView({
+ model: TheTicket,
+ wikiContext: "ticket:",
+ use_overlay: true,
+ Caption: "Edit Comment text",
+ OKLabel: "Add Comment",
+ CancelLabel: "Abandon changes to comment",
+ readonly: !editable,
+ srcattr: "comment",
+ el: "#commentedit"
+ });
+
+ function refresh_lists() {
+ var Changes = TheTicket.getChanges();
+ var recent = Changes.at(0);
+ if (!recent) {
+ return;
+ }
+ Changes.fetch({
+ success: function (c, r) {
+ if (r.length) {
+ TheTicket.getAttachments().fetch({
+ success: function () {
+ attachments = TheTicket.getAttachments().models;
+ }
+ });
+ if (!TheTicket.changed && TheTicket.mtrack_edit_count == 0) {
+ TheTicket.mtrack_fetching = true;
+ TheTicket.fetch({
+ error: function() {
+ TheTicket.mtrack_fetching = false;
+ },
+ success: function (model, resp) {
+ TheTicket.mtrack_fetching = false;
+ if (model.get('updated').cid != base_ticket.updated.cid) {
+ /* it changed */
+ base_ticket = TheTicket.toJSON();
+ TheTicket.unset('comment', {silent: true});
+ editor.render();
+ }
+ }
+ });
+ }
+ }
+ changes = Changes.models;
+ },
+ data: {
+ last:
recent.id
+ },
+ add: true
+ });
+ }
+
+ function check_for_changes() {
+ refresh_lists();
+ }
+ if (!TheTicket.isNew()) {
+ setInterval(check_for_changes, 60000);
+ }
+
+ $('#issue-error').click(function () {
+ $(this).fadeOut('fast');
+ });
+
+ $('#save-issue').click(function () {
+ $('#issue-error').fadeOut('fast');
+ var overlay = $('<div class="overlay"/>');
+ overlay.appendTo('body').fadeIn('fast', function () {
+ TheTicket.save(TheTicket.toJSON(), {
+ success: function(model, response) {
+ $('#issue-buttons button.hide-until-change').fadeOut('fast');
+ overlay.fadeOut('fast', function () {
+ if (base_ticket.nsident == null) {
+ /* we just saved the initial version; revise the URL
+ * to reflect our new status */
+ var url = ABSWEB + 'ticket.php/' + TheTicket.get('nsident');
+ window.onbeforeunload = null;
+ window.location.href = url;
+ return;
+ }
+ base_ticket = TheTicket.toJSON();
+ TheTicket.unset('comment', {silent: true});
+ editor.render();
+ refresh_lists();
+ overlay.remove();
+ TheTicket.changed = false;
+ });
+ },
+ error: function(model, response) {
+ var err;
+ var conflict = null;
+ if (!_.isObject(response)) {
+ err = response;
+ } else {
+ err = response.statusText;
+ try {
+ var r = JSON.parse(response.responseText);
+ err = r.message;
+ if (r.code == 409) {
+ conflict = r.extra;
+ }
+ } catch (e) {
+ err = response.statusText;
+ }
+ }
+ refresh_lists();
+
+ if (conflict) {
+ var tbl = $('#conflict-list');
+ tbl.empty();
+ TheTicket.set({updated: conflict.updated});
+ delete conflict.updated;
+ for (var k in conflict) {
+ if (k == 'description_html') {
+ continue;
+ }
+ var item = conflict[k];
+ var o = {
+ field: k,
+ yours: item[0],
+ theirs: item[1]
+ };
+ $(_.template(
+ "<tr><td><%- field %></td><td><%- yours %></td><td><%- theirs %></td></tr>", o)).
+ appendTo(tbl);
+ }
+ $('#conflict-form').fadeIn('fast');
+ $('#conflict-keep').click(function () {
+ $('#conflict-form').fadeOut('fast');
+ overlay.fadeOut('fast', function () {
+ overlay.remove();
+ });
+ return false;
+ });
+ $('#conflict-take').click(function () {
+ $('#conflict-form').fadeOut('fast');
+ var o = {};
+ for (var k in conflict) {
+ var item = conflict[k];
+ o[k] = item[1];
+ }
+ TheTicket.set(o);
+
+ overlay.fadeOut('fast', function () {
+ overlay.remove();
+ });
+ return false;
+ });
+ } else {
+ $('#issue-error-text').text(err);
+ $('#issue-error').fadeIn('fast')
+ overlay.fadeOut('fast', function () {
+ overlay.remove();
+ });
+ }
+ }
+ });
+ });
+ });
+
+ $('#cancel-issue').click(function () {
+ $('#issue-error').fadeOut('fast');
+ reset_editor();
+ $('#issue-buttons button.hide-until-change').fadeOut('fast');
+ });
+
+ var in_reply = false;
+ var orig_comment = null;
+
+ if (editable) {
+ comment_editor.bind('canceledit', function () {
+ console.log("cancel");
+ if (in_reply) {
+ in_reply = false;
+ TheTicket.set({comment: orig_comment},{silent:true});
+ }
+ });
+ TheTicket.bind('change:comment', function () {
+ in_reply = false;
+ orig_comment = null;
+ });
+ $('#comment-issue').click(function () {
+ comment_editor.edit();
+ return false;
+ });
+ } else {
+ $('#comment-issue').hide();
+ }
+
+ function reply_comment(cid) {
+ var c = TheTicket.getChanges().get(cid);
+ orig_comment = TheTicket.get("comment");
+ var comment = orig_comment || '';
+ if (comment.length) {
+ comment = comment + "\\n\\n";
+ }
+ var reason = c.get('reason');
+ // cite it
+ reason = reason.replace(/^(\s*)/mg, "> \$1");
+ comment = comment + "Replying to [comment:" + cid + " a comment by " +
+ c.get('who') + "]\\n" + reason + "\\n";
+ in_reply = true;
+ TheTicket.set({'comment': comment}, {silent: true});
+ comment_editor.edit();
+ }
+ window.mtrack_reply_comment = reply_comment;
+
+
+ function calc_soff() {
+ var b = $('#issue-buttons');
+ var d = b.position().top - $(window).scrollTop() + b.height() + 10;
+ return d;
+ }
+
+ var clicking = false;
+ /* color the "tabs" based on the scroll position */
+ function highlight_tab() {
+ var soff = calc_soff();
+ var y = $(window).scrollTop();
+ var active = null;
+ $('#issue-content a').each(function () {
+ var what = $(this).attr('href');
+ var target = $(what);
+
+ var pos = $(target).position()['top'] - soff;
+ $(this).removeClass('active');
+ if (y >= pos) {
+ active = $(this);
+ }
+ });
+ if (active) {
+ active.addClass('active');
+ }
+ }
+ $(window).scroll(function() {
+ if (!clicking) {
+ highlight_tab();
+ }
+ });
+
+ $('#issue-content a').click(function () {
+ var what = $(this).attr('href');
+ var target = $(what);
+ var d = calc_soff();
+ var t = target.offset().top - d;
+ clicking = true;
+ $('#issue-content a').removeClass('active');
+ $(this).addClass('active');
+ $('html, body').animate(
+ {scrollTop: t},
+ 350,
+ 'easeOutQuint',
+ function () {
+ clicking = false;
+ }
+ );
+ return false;
+ });
+});
+</script>
+HTML;
+
+mtrack_foot();
+
https://bitbucket.org/wez/mtrack/changeset/16c55bd3bc68/
changeset: 16c55bd3bc68
user: wez
date: 2012-04-26 05:45:01
summary: re-enable the reply-to-comment feature.
When saving, replace and reload the current URL; there's some kind of
changeset id edge case that makes the comment list not update and its
easier (and cleaner!) just to reload.
affected #: 3 files
diff -r 8c5ed4e4569ce7bef9a6b4b16c82350b6c3a47f4 -r 16c55bd3bc68a7111d3929cb37809d96ed209201 web/js/templates/ticket.event.show.html
--- a/web/js/templates/ticket.event.show.html
+++ b/web/js/templates/ticket.event.show.html
@@ -1,7 +1,7 @@
<div class='ticketevent'><a class='pmark' href='#<%- id %>'>#</a><a name='<%- id %>'> </a><abbr class='timeinterval' title='<%- changedate %>'><%- changedate %></abbr><%- who %>
- <!-- a class='replycomment'
- href="javascript:mtrack_reply_comment(<%- id %>);">reply</a -->
+ <a class='replycomment'
+ href="javascript:mtrack_reply_comment(<%- id %>);">reply</a></div><div class='ticketchangeinfo'><img class='gravatar' src="<%= ABSWEB %>avatar.php?u=<%- who %>&s=48">
diff -r 8c5ed4e4569ce7bef9a6b4b16c82350b6c3a47f4 -r 16c55bd3bc68a7111d3929cb37809d96ed209201 web/js/templates/ticket.show.html
--- a/web/js/templates/ticket.show.html
+++ b/web/js/templates/ticket.show.html
@@ -76,6 +76,9 @@
padding-left: 60px;
min-height: 64px;
}
+ div#tkt-comments div.ticketevent a.replycomment {
+ float: none;
+ }
div#tkt-comments div.ticketevent {
border-bottom: solid 1px #eee;
}
diff -r 8c5ed4e4569ce7bef9a6b4b16c82350b6c3a47f4 -r 16c55bd3bc68a7111d3929cb37809d96ed209201 web/ticket.php
--- a/web/ticket.php
+++ b/web/ticket.php
@@ -697,14 +697,7 @@
}
});
}
- function disable_change_refresh() {
- clearInterval(view.timer);
- view.timer = null;
- }
- function enable_change_refresh() {
- view.timer = setInterval(refresh_changes, 60000);
- }
- enable_change_refresh();
+ view.refresh = refresh_changes;
var user_template = _.template(mtrack_underscore_templates['user-name']);
function render_user(user) {
@@ -826,7 +819,8 @@
});
editor.show({
success: function (model) {
- view.render();
+ window.location.replace(
+ ABSWEB + 'ticket.php/' + model.get('nsident'));
},
hidden: function () {
refresh_changes();
@@ -901,6 +895,27 @@
}
});
}
+
+ window.mtrack_reply_comment = function (cid) {
+ var c = view.model.getChanges().get(cid);
+ orig_comment = view.model.get("comment");
+ var comment = orig_comment || '';
+ if (comment.length) {
+ comment = comment + "\\n\\n";
+ }
+ var reason = c.get('reason');
+ // cite it
+ reason = reason.replace(/^(\s*)/mg, "> \$1");
+ comment = comment + "Replying to [comment:" + cid + " a comment by " +
+ c.get('who') + "]\\n" + reason + "\\n";
+ in_reply = true;
+ view.model.set({'comment': comment});
+
+ $('#editbtn', this.el).trigger('click');
+
+ return false;
+ }
+
return this;
}
});
@@ -930,6 +945,11 @@
el: $('#ticket')
});
V.render();
+
+ setInterval(function () {
+ V.refresh()
+ }, 60000);
+
});
</script>
HTML;
https://bitbucket.org/wez/mtrack/changeset/0aaf38c83cc7/
changeset: 0aaf38c83cc7
user: wez
date: 2012-04-26 06:00:53
summary: move view code to views file so that it can be re-used for plan.php
affected #: 3 files
diff -r 16c55bd3bc68a7111d3929cb37809d96ed209201 -r 0aaf38c83cc7e0c21fad6fe293dd4f8698b2fcf2 web/js/templates/ticket.show.html
--- a/web/js/templates/ticket.show.html
+++ b/web/js/templates/ticket.show.html
@@ -58,7 +58,7 @@
font-size: 0.8em;
white-space: nowrap;
color: #777;
- max-width: 8.5em;
+ max-width: 9em;
overflow: hidden;
text-overflow: ellipsis;
}
diff -r 16c55bd3bc68a7111d3929cb37809d96ed209201 -r 0aaf38c83cc7e0c21fad6fe293dd4f8698b2fcf2 web/js/views.js
--- a/web/js/views.js
+++ b/web/js/views.js
@@ -2797,6 +2797,876 @@
dlg.modal('show');
}
-
});
+// Ticket editor
+var MTrackTicketEditor = Backbone.View.extend({
+ show: function(on_success) {
+ var o = this.model.toJSON();
+ o.isnew = this.model.isNew();
+ // A clone of the model to use for editing with the existing
+ // set of editors
+ var dup_model = this.model.clone();
+ dup_model.getAttachments().reset(this.model.getAttachments().models);
+
+ $(this.el).html(_.template(
+ mtrack_underscore_templates['ticket-edit'], o));
+
+ var view = this;
+ $(view.el).appendTo('body');
+ var dlg = $('div.modal', view.el);
+ var in_wiki = false;
+
+ dlg.on('hidden', function () {
+ if (!in_wiki) {
+ $(view.el).remove();
+ if ('hidden' in on_success) {
+ on_success.hidden();
+ }
+ }
+ });
+
+ // Validation errors
+ dup_model.bind('error', function (model, err) {
+ mtrack_ajax_error_to_dom(err, $('div.modal-header', view.el));
+ });
+
+ var attach_list = $('#attach-list', dlg);
+ attach_list.on('click', 'button.delattach', function() {
+ var att = $(this).closest('div.attachment').data('attachment-model');
+ var m = $("<div class='modal fade'><div class='modal-header'><a class='close' data-dismiss='modal'>x</a><h3>Delete Attachment?</h3></div><div class='modal-body'><p><b></b></p><p>Do you really want to delete this attachment?</p><p>You cannot undo this action!</p></div><div class='modal-footer'><button class='btn' data-dismiss='modal'>Close</button><button class='btn btn-danger'>Delete</button></div></div>");
+
+ $('b', m).text(att.get('filename'));
+ $('.btn-danger', m).click(function () {
+ att.destroy();
+ m.modal('hide');
+ });
+
+ m.on('hidden', function() {
+ m.remove();
+ });
+ m.modal();
+
+ });
+ function redraw_attachment_list() {
+ attach_list.empty();
+ var t = _.template(mtrack_underscore_templates['attachment-item-edit']);
+
+ dup_model.getAttachments().each(function (att) {
+ var o = att.toJSON();
+ if ('width' in o) {
+ o.image = true;
+ } else {
+ o.image = false;
+ }
+ var d = $(t(o));
+ d.data('attachment-model', att);
+ attach_list.append(d);
+ });
+ $('.timeinterval', attach_list).timeago();
+ }
+ redraw_attachment_list();
+ dup_model.getAttachments().bind('all', function () {
+ redraw_attachment_list();
+ });
+
+ // Grab template elements and take them out of the DOM
+ var tab_ul = $('ul.nav-tabs', view.el);
+ var tab_hdr = $('li:first', tab_ul);
+ tab_hdr.remove();
+ var tab_att = $('li', tab_ul);
+ tab_att.remove(); // we'll append it at the end
+ var tab_content = $('div.tab-content', view.el);
+ var tab_body = $('div.tab-pane:first', tab_content);
+ tab_body.remove();
+
+ var next_tab_id = 1;
+ function add_group_tab(group) {
+ var label;
+ if (
group.name == 0) {
+ label = 'Details';
+ } else {
+ label =
group.name;
+ }
+
+ var id = 'tab-' + next_tab_id++;
+
+ var hdr = tab_hdr.clone();
+ $('a', hdr).attr('href', '#' + id);
+ $('a', hdr).text(label);
+ tab_ul.append(hdr);
+
+ var tab = tab_body.clone();
+ tab.attr('id', id);
+ tab_content.append(tab);
+
+ if (next_tab_id == 2) {
+ // Make the first one active
+ $('a', hdr).trigger('click');
+ }
+
+ return tab;
+ }
+
+ function multi_editor(lcont, tab, field) {
+ var label = $('<label/>');
+ label.text(field.label);
+ tab.append(label);
+
+ var edit = $('<textarea/>', {
+ cols: field.cols,
+ rows: field.rows,
+ placeholder: field.placeholder
+ });
+ var val = dup_model.get(
field.name);
+ if (val) {
+ edit.val(val);
+ }
+ edit.on('change', function() {
+ var o = {};
+ o[
field.name] = edit.val();
+ dup_model.set(o);
+ });
+ lcont.remove();
+ tab.append(edit);
+ tab.attr('colspan', 2);
+ }
+
+ function wiki_editor(lcont, tab, field) {
+ var label = $('<label/>');
+ label.text(field.label);
+ tab.append(label);
+
+ var edit = $('<textarea/>', {
+ class: 'wiki shortwiki',
+ cols: field.cols,
+ rows: field.rows,
+ placeholder: field.placeholder
+ });
+ var val = dup_model.get(
field.name);
+ if (val) {
+ edit.val(val);
+ }
+ lcont.remove();
+ tab.append(edit);
+ var b = $('<button/>', {
+ class: 'btn'
+ });
+ b.html('<i class="icon-pencil"></i> Edit ' + field.label + ' in wiki editor');
+ tab.append(b);
+ tab.attr('colspan', 2);
+
+ dup_model.bind('change:' +
field.name, function () {
+ var val = dup_model.get(
field.name);
+ edit.val(val ? val : '');
+ });
+ edit.on('change', function() {
+ var o = {};
+ o[
field.name] = edit.val();
+ dup_model.set(o);
+ });
+
+ var wiki = new MTrackWikiTextAreaView({
+ model: dup_model,
+ wikiContext: 'ticket:',
+ use_overlay: true,
+ Caption: "Edit " + field.label,
+ OKLabel: "Accept " + field.label,
+ CancelLabel: "Abandon changes to " + field.label,
+ srcattr:
field.name,
+ renderedattr:
field.name + '_html'
+ });
+ wiki.bind('editstart', function () {
+ in_wiki = true;
+ setTimeout(function () {
+ dlg.modal('hide');
+ }, 1000);
+ });
+ wiki.bind('editend', function () {
+ in_wiki = false;
+ dlg.modal('show');
+ });
+
+ b.click(function () {
+ var o = {};
+ o[
field.name] = edit.val();
+ dup_model.set(o);
+ wiki.edit();
+ });
+ }
+
+ function text_editor(lcont, tab, field) {
+ lcont.text(field.label);
+
+ var inp = $('<input/>', {
+ type: "text",
+ name:
field.name,
+ placeholder: field.placeholder
+ });
+ var val = view.model.get(
field.name);
+ if (val) {
+ inp.val(val);
+ }
+ inp.on('change', function() {
+ var o = {};
+ o[
field.name] = inp.val();
+ dup_model.set(o);
+ });
+
+ tab.append(inp);
+ }
+
+ function multiselect_editor(lcont, tab, field) {
+ lcont.text(field.label);
+
+ var el = $('<div/>');
+ tab.append(el);
+ var view = new MTrackSelectEditorView({
+ el: el,
+ model: dup_model,
+ multiple: true,
+ srcattr:
field.name,
+ label: field.label,
+ width: '418px',
+ values: field.options,
+ defval: field["default"],
+ placeholder: field.placeholder
+ });
+ view.render();
+ }
+
+ function select_editor(lcont, tab, field) {
+ lcont.text(field.label);
+
+ var el = $('<span/>');
+ tab.append(el);
+ var view = new MTrackSelectEditorView({
+ el: el,
+ model: dup_model,
+ srcattr:
field.name,
+ label: field.label,
+ width: '418px',
+ values: field.options,
+ defval: field["default"],
+ placeholder: field.placeholder
+ });
+ view.render();
+ }
+
+ function ticketdeps_editor(lcont, tab, field) {
+ lcont.text(field.label);
+
+ var el = $('<span/>');
+ tab.append(el);
+ var view = new MTrackTicketDepEditView({
+ el: el,
+ model: dup_model,
+ srcattr:
field.name,
+ label: field.label,
+ });
+ view.render();
+ }
+
+ function tags_editor(lcont, tab, field) {
+ lcont.text(field.label);
+
+ var el = $('<span/>');
+ tab.append(el);
+ var view = new MTrackTagEditView({
+ el: el,
+ model: dup_model,
+ srcattr:
field.name,
+ label: field.label,
+ });
+ view.render();
+ }
+
+ function cc_editor(lcont, tab, field) {
+ lcont.text(field.label);
+
+ var el = $('<span/>');
+ tab.append(el);
+ var view = new MTrackCcEditView({
+ el: el,
+ model: dup_model,
+ srcattr:
field.name,
+ label: field.label,
+ });
+ view.render();
+ }
+
+ var editors = {
+ multi: multi_editor,
+ select: select_editor,
+ multiselect: multiselect_editor,
+ tags: tags_editor,
+ cc: cc_editor,
+ ticketdeps: ticketdeps_editor,
+ text: text_editor,
+ wiki: wiki_editor
+ };
+
+ function add_editor(tr, tab, field)
+ {
+ if (field.type == 'readonly') return;
+
+ var editor = text_editor;
+
+ if (field.type in editors) {
+ editor = editors[field.type];
+ }
+ tr = tr.clone();
+ var lcont = $('td.fieldname', tr);
+ var div = $('td.fieldvalue', tr);
+ tab.append(tr);
+ editor(lcont, div, field);
+ }
+
+ function add_tab_and_fields(group) {
+ var tab = add_group_tab(group);
+
+ var table = $('table', tab);
+ var tr = $('tr', table);
+ tr.remove();
+
+ if (
group.name == 0) {
+ // Synthesize some fields
+ add_editor(tr, table, {
+ name: 'summary',
+ label: 'Summary'
+ });
+ add_editor(tr, table, {
+ name: 'status',
+ label: 'Status',
+ type: 'select',
+ options: mtrack_ticket_states
+ });
+ if (dup_model.get('status') != 'closed') {
+ add_editor(tr, table, {
+ name: 'resolution',
+ label: 'Resolution',
+ placeholder: 'Resolve ticket as...',
+ type: 'select',
+ options: mtrack_resolutions
+ });
+ }
+ }
+ _.each(group.fields, function (field) {
+ add_editor(tr, table, field);
+ });
+ }
+
+ var com_tab = add_tab_and_fields({
+ name: 'Comment',
+ fields: [
+ {
+ name: 'comment',
+ label: 'Comment',
+ type: 'wiki',
+ placeholder: 'Something on your mind? Share it here!',
+ rows: 10,
+ cols: 78
+ }
+ ]
+ });
+
+ /* Create a tab for each category of field */
+ for (var gidx in this.options.fields) {
+ add_tab_and_fields(this.options.fields[gidx]);
+ }
+ tab_ul.append(tab_att);
+
+ /* handle uploads */
+ var uploading = false;
+ $('#confirm-upload', dlg).click(function () {
+ uploading = true;
+ $('#upload-form', dlg).submit();
+ });
+ $('#upload_target', dlg).on('load', function () {
+ var res = $(this).contents().find('body').text();
+ try {
+ res = JSON.parse(res);
+ if (res.status == 'success') {
+ if (uploading) {
+ $('<div class="alert alert-success">' +
+ '<a class="close" data-dismiss="alert">×</a>' +
+ 'Upload successful</div>').
+ appendTo($('#tkt-edit-attachments', dlg));
+ dup_model.getAttachments().reset(res.attachments);
+ }
+ $('input[type=file]', dlg).val('');
+ } else {
+ $('<div class="alert alert-danger">' +
+ '<a class="close" data-dismiss="alert">×</a>' +
+ res.message + '</div>').
+ appendTo($('#tkt-edit-attachments', dlg));
+ }
+ } catch (e) {
+ }
+ uploading = false;
+ });
+
+ // Present the conflict resolution UI.
+ // This is an alternative form that shows the changes side-by-side
+ // and allows the user to pick a resolution:
+ // - Accept my changes
+ // - Accept their changes
+ // - Cancel
+ // The first two will re-display the edit dialog with the updated
+ // model, the latter will cancel the edit dialog.
+ function show_conflict_resolver(conflict) {
+ var updated = conflict.updated;
+ delete conflict.updated;
+ delete conflict.description_html;
+ var o = {
+ nsident: dup_model.get('nsident'),
+ summary: dup_model.get('summary'),
+ conflict: conflict,
+ updated: updated,
+ ABSWEB: ABSWEB
+ };
+ var CD = $(_.template(mtrack_underscore_templates['ticket-conflict'], o));
+ $('body').append(CD);
+ $('.timeinterval', CD).timeago();
+
+ // Fixup model so that we don't trigger a 409 on next save
+ // (unless there is a further conflict!)
+ dup_model.set({updated: o.updated});
+
+ dlg.modal('hide');
+ CD.modal('show');
+ CD.on('hidden', function() {
+ CD.remove();
+ });
+
+ $('button.mine', CD).click(function () {
+ var editor = new MTrackTicketEditor({
+ model: dup_model,
+ fields: view.options.fields
+ });
+ CD.modal('hide');
+ editor.show(on_success);
+ });
+
+ $('button.theirs', CD).click(function () {
+ var o = {};
+ for (var k in conflict) {
+ var item = conflict[k];
+ o[k] = item[1];
+ }
+ dup_model.set(o);
+
+ var editor = new MTrackTicketEditor({
+ model: dup_model,
+ fields: view.options.fields
+ });
+ CD.modal('hide');
+ editor.show(on_success);
+ });
+ }
+ /*
+ $('.modal-footer button.conflict', dlg).click(function () {
+ show_conflict_resolver({
+ updated: {
+ who: 'otherguy',
+ when: "2012-04-11T14:54:36+00:00",
+ cid: "17"
+ },
+ summary: ['my lemons', 'your lemons']
+ });
+ });
+ */
+
+ // Save. We want to apply the attributes from the dup_model to
+ // the real model.
+ $('.modal-footer button.btn-primary', dlg).click(function () {
+ view.model.save(dup_model.attributes, {
+ success: function(model) {
+ if ('success' in on_success) {
+ on_success.success(model);
+ }
+ dlg.modal('hide');
+ },
+ error: function (model, resp) {
+ // If a conflict was detected, show some useful UI to help
+ // them through it
+ var is_conflict = false;
+
+ if (_.isObject(resp)) {
+ try {
+ var r = JSON.parse(resp.responseText);
+ if (r.code == 409) {
+ is_conflict = r.extra;
+ }
+ } catch (e) {
+ }
+ }
+
+ if (!is_conflict) {
+ mtrack_ajax_error_to_dom(resp, $('div.modal-header', dlg));
+ return;
+ }
+
+ show_conflict_resolver(is_conflict);
+ }
+ });
+ });
+
+ dlg.modal('show');
+ }
+});
+
+// Ticket viewer
+var MTrackTicketViewer = Backbone.View.extend({
+ render: function() {
+ var t = _.template(mtrack_underscore_templates['ticket-show']);
+ var o = this.model.toJSON();
+ $(this.el).html(t(o));
+// $('body').attr('data-target', '#tkt-nav');
+// $('body').scrollspy({offset: 30});
+
+ var F = $('#tkt-fields');
+ // Table row template
+ var TR = F.find('tr');
+ var table = TR.parent();
+ TR.remove();
+
+ var model = this.model;
+ var view = this;
+
+ var attach_list = $('#attach-list', this.el);
+ function redraw_attachment_list() {
+ attach_list.empty();
+ var t = _.template(mtrack_underscore_templates['attachment-item']);
+ var count = 0;
+
+ model.getAttachments().each(function (att) {
+ count++;
+ var o = att.toJSON();
+ if ('width' in o) {
+ o.image = true;
+ } else {
+ o.image = false;
+ }
+ var d = $(t(o));
+ d.data('attachment-model', att);
+ attach_list.append(d);
+ });
+ if (count) {
+ $('.timeinterval', attach_list).timeago();
+ $('#attach', view.el).show();
+ $('.tkt-outline a[href=#attach]').show();
+ } else {
+ $('#attach', view.el).hide();
+ $('.tkt-outline a[href=#attach]').hide();
+ }
+ }
+ redraw_attachment_list();
+ model.getAttachments().bind('all', function () {
+ redraw_attachment_list();
+ });
+
+ var change_tpl = _.template(
+ mtrack_underscore_templates['ticket-event-show']);
+ var change_cont = $('#tkt-comments', this.el);
+
+ function add_one_change(cs) {
+ var d = $('<div/>');
+ $(d).html(change_tpl(cs.toJSON()));
+ var had_comment = false;
+ var commit = false;
+ _.each(cs.get('audit'), function (a) {
+ if (a.label == 'Comment') {
+ had_comment = true;
+ if (a.value.match(/^\(In /)) {
+ commit = true;
+ }
+ }
+ });
+ if (had_comment) {
+ d.addClass('chg-comment');
+ if (commit) {
+ d.addClass('chg-commit');
+ }
+ } else {
+ d.addClass('chg-no-comment');
+ }
+ d.appendTo(change_cont);
+ $('.toggle-desc', d).click(function () {
+ $('#' + $(this).attr('desc-id')).toggle();
+ return false;
+ });
+ $('.timeinterval', d).timeago();
+ }
+ function redraw_change_list() {
+ model.getChanges().each(function (cs) {
+ add_one_change(cs);
+ });
+ }
+ redraw_change_list();
+
+ function refresh_changes() {
+ var changes = model.getChanges();
+ var recent =
changes.at(0);
+
+ // If a modal dialog is open, don't do any updating
+ if ($('body').hasClass('modal-open')) {
+ return;
+ }
+
+ if (!recent) {
+ return;
+ }
+
+ changes.fetch({
+ success: function (c, r) {
+ if (!r.length) {
+ return;
+ }
+ var latest =
changes.at(0);
+ if (
latest.id ==
recent.id) {
+ // No change
+ return;
+ }
+ model.getAttachments().fetch();
+ var base_cid = model.get('updated').cid;
+ model.fetch({
+ success: function (model, resp) {
+ if (model.get('updated').cid == base_cid) {
+ return;
+ }
+ /* it changed */
+ model.unset('comment', {silent: true});
+ view.render();
+ }
+ });
+ }
+ });
+ }
+ view.refresh = refresh_changes;
+
+ var user_template = _.template(mtrack_underscore_templates['user-name']);
+ function render_user(user) {
+ if (!_.isObject(user)) {
+ user = {
+ id: user,
+ label: user
+ };
+ }
+ var o = _.clone(user);
+ o.ABSWEB = ABSWEB;
+ return user_template(o);
+ }
+
+ function render_user_list(users) {
+ var res = [];
+ _.each(users, function(user) {
+ res.push(render_user(user));
+ });
+ return res.join(' ');
+ }
+
+ function render_change_time(item) {
+ item = _.clone(item);
+ item.ABSWEB = ABSWEB;
+ return _.template(mtrack_underscore_templates['item-changed'], item);
+ }
+
+ var renderers = {
+ 'owner': render_user,
+ 'cc': render_user_list,
+ 'created': render_change_time,
+ 'updated': render_change_time,
+ };
+
+ function process_field(field) {
+ if (
field.name == 'description') {
+ return;
+ }
+ var val = model.get(
field.name);
+ if (typeof(val) == 'undefined' || val == null) {
+ return;
+ }
+ if (_.isArray(val) && val.length == 0) {
+ return;
+ }
+ if (typeof(val) == 'string' && val == '') {
+ return;
+ }
+ var tr = TR.clone();
+ $('td.fieldname', tr).text(field.label).attr('title', field.label);
+ if (
field.name in renderers) {
+ $('td.fieldvalue', tr).html(renderers[
field.name](val));
+ } else {
+ // If we have a template defined, then use the generic template
+ // based renderer. First look to see if we have a template
+ // for the specific field name, then try to fall back to a
+ // generic formatter by type.
+ var tplname = "ticket-field-byname-" +
field.name;
+ if (!(tplname in mtrack_underscore_templates)) {
+ // Try by type
+ tplname = "ticket-field-bytype-" + field.type;
+ }
+
+ var render = function (a) {
+ return $('<div/>').text(a).html();
+ };
+
+ if (tplname in mtrack_underscore_templates) {
+ var t = _.template(mtrack_underscore_templates[tplname]);
+ render = function (a) {
+ /* wrap it up so that the value itself is accessible,
+ * as some of the data types we have encode the ids
+ * in the keys of an object and we can't iterate
+ * the context without a name in the template handler */
+ var o = {
+ value: a,
+ ABSWEB: ABSWEB
+ };
+ return t(o);
+ };
+ }
+ $('td.fieldvalue', tr).html(render(val));
+ }
+ table.append(tr);
+ }
+
+ if (model.get('status') == 'closed') {
+ process_field({name: 'resolution', label: 'Resolved'});
+ } else {
+ process_field({name: 'status', label: 'Status'});
+ }
+ if (model.get('parent')) {
+ process_field({name: 'parent', label: 'Parent', type: 'ticket'});
+ }
+
+ process_field({name: 'created', label: 'Opened'});
+ if (model.get('updated') &&
+ model.get('updated').cid != model.get('created').cid) {
+ process_field({name: 'updated', label: 'Updated'});
+ }
+ if (model.get('remaining')) {
+ process_field({name: 'remaining', label: 'Remaining Time'});
+ }
+
+ for (var gidx in this.options.fields) {
+ var group = this.options.fields[gidx];
+ _.each(group.fields, process_field);
+ }
+ $('.timeinterval', this.el).timeago();
+
+ $('#togglebtn', this.el).click(function () {
+ $('#tkt-fields', this.el).toggle();
+ return false;
+ });
+
+ // Open the ticket editor
+ $('#editbtn', this.el).click(function () {
+ var editor = new MTrackTicketEditor({
+ model: view.model,
+ fields: view.options.fields
+ });
+ editor.show({
+ success: function (model) {
+ window.location.replace(
+ ABSWEB + 'ticket.php/' + model.get('nsident'));
+ },
+ hidden: function () {
+ refresh_changes();
+ }
+ });
+ return false;
+ });
+
+ /* operates on the result of a ticket split; if saved,
+ * we're taken to the ticket page for the newly saved ticket */
+ function edit_split(model) {
+ var editor = new MTrackTicketEditor({
+ model: model,
+ fields: view.options.fields
+ });
+ editor.show({
+ success: function (model) {
+ /* go to that ticket page */
+ window.location = ABSWEB + 'ticket.php/' + model.get('nsident');
+ },
+ hidden: function () {
+ refresh_changes();
+ }
+ });
+ return false;
+ }
+
+ function make_split_ticket() {
+ var S = view.model.clone();
+ console.log("cloned as", S);
+ S.unset('spent');
+ S.unset('remaining');
+ S.unset('estimated');
+ S.unset('nsident');
+ S.unset('id');
+ S.unset('created');
+ S.unset('updated');
+ S.set({children: []});
+ S.set({description:
+ "\nSplit from #" + view.model.get('nsident') + "\n\n---\n\n" +
+ view.model.get('description')});
+ // Pointless to clone it in a closed state
+ if (S.get('status') == 'closed') {
+ S.set({status: 'open'});
+ }
+ S.unset('resolution');
+ return S;
+ }
+ $('#splitsib', this.el).click(function () {
+ edit_split(make_split_ticket());
+ return false;
+ });
+ $('#splitchild', this.el).click(function () {
+ var S = make_split_ticket();
+ // New ticket is a child of the current one
+ S.set({ptid:
view.model.id});
+ edit_split(S);
+ return false;
+ });
+
+ if (view.model.isNew()) {
+ var editor = new MTrackTicketEditor({
+ model: view.model,
+ fields: view.options.fields
+ });
+ editor.show({
+ success: function (model) {
+ window.location = ABSWEB + 'ticket.php/' + model.get('nsident');
+ },
+ hidden: function () {
+ refresh_changes();
+ }
+ });
+ }
+
+ window.mtrack_reply_comment = function (cid) {
+ var c = view.model.getChanges().get(cid);
+ orig_comment = view.model.get("comment");
+ var comment = orig_comment || '';
+ if (comment.length) {
+ comment = comment + "\\n\\n";
+ }
+ var reason = c.get('reason');
+ // cite it
+ reason = reason.replace(/^(\s*)/mg, "> \$1");
+ comment = comment + "Replying to [comment:" + cid + " a comment by " +
+ c.get('who') + "]\\n" + reason + "\\n";
+ in_reply = true;
+ view.model.set({'comment': comment});
+
+ $('#editbtn', this.el).trigger('click');
+
+ return false;
+ }
+
+ return this;
+ }
+});
+
+
diff -r 16c55bd3bc68a7111d3929cb37809d96ed209201 -r 0aaf38c83cc7e0c21fad6fe293dd4f8698b2fcf2 web/ticket.php
--- a/web/ticket.php
+++ b/web/ticket.php
@@ -53,872 +53,6 @@
echo <<<HTML
<div id="ticket"></div><script type='text/javascript'>
-// Ticket editor
-var TE = Backbone.View.extend({
- show: function(on_success) {
- var o = this.model.toJSON();
- o.isnew = this.model.isNew();
- // A clone of the model to use for editing with the existing
- // set of editors
- var dup_model = this.model.clone();
- dup_model.getAttachments().reset(this.model.getAttachments().models);
-
- $(this.el).html(_.template(
- mtrack_underscore_templates['ticket-edit'], o));
-
- var view = this;
- $(view.el).appendTo('body');
- var dlg = $('div.modal', view.el);
- var in_wiki = false;
-
- dlg.on('hidden', function () {
- if (!in_wiki) {
- $(view.el).remove();
- if ('hidden' in on_success) {
- on_success.hidden();
- }
- }
- });
-
- // Validation errors
- dup_model.bind('error', function (model, err) {
- mtrack_ajax_error_to_dom(err, $('div.modal-header', view.el));
- });
-
- var attach_list = $('#attach-list', dlg);
- attach_list.on('click', 'button.delattach', function() {
- var att = $(this).closest('div.attachment').data('attachment-model');
- var m = $("<div class='modal fade'><div class='modal-header'><a class='close' data-dismiss='modal'>x</a><h3>Delete Attachment?</h3></div><div class='modal-body'><p><b></b></p><p>Do you really want to delete this attachment?</p><p>You cannot undo this action!</p></div><div class='modal-footer'><button class='btn' data-dismiss='modal'>Close</button><button class='btn btn-danger'>Delete</button></div></div>");
-
- $('b', m).text(att.get('filename'));
- $('.btn-danger', m).click(function () {
- att.destroy();
- m.modal('hide');
- });
-
- m.on('hidden', function() {
- m.remove();
- });
- m.modal();
-
- });
- function redraw_attachment_list() {
- attach_list.empty();
- var t = _.template(mtrack_underscore_templates['attachment-item-edit']);
-
- dup_model.getAttachments().each(function (att) {
- var o = att.toJSON();
- if ('width' in o) {
- o.image = true;
- } else {
- o.image = false;
- }
- var d = $(t(o));
- d.data('attachment-model', att);
- attach_list.append(d);
- });
- $('.timeinterval', attach_list).timeago();
- }
- redraw_attachment_list();
- dup_model.getAttachments().bind('all', function () {
- redraw_attachment_list();
- });
-
- // Grab template elements and take them out of the DOM
- var tab_ul = $('ul.nav-tabs', view.el);
- var tab_hdr = $('li:first', tab_ul);
- tab_hdr.remove();
- var tab_att = $('li', tab_ul);
- tab_att.remove(); // we'll append it at the end
- var tab_content = $('div.tab-content', view.el);
- var tab_body = $('div.tab-pane:first', tab_content);
- tab_body.remove();
-
- var next_tab_id = 1;
- function add_group_tab(group) {
- var label;
- if (
group.name == 0) {
- label = 'Details';
- } else {
- label =
group.name;
- }
-
- var id = 'tab-' + next_tab_id++;
-
- var hdr = tab_hdr.clone();
- $('a', hdr).attr('href', '#' + id);
- $('a', hdr).text(label);
- tab_ul.append(hdr);
-
- var tab = tab_body.clone();
- tab.attr('id', id);
- tab_content.append(tab);
-
- if (next_tab_id == 2) {
- // Make the first one active
- $('a', hdr).trigger('click');
- }
-
- return tab;
- }
-
- function multi_editor(lcont, tab, field) {
- var label = $('<label/>');
- label.text(field.label);
- tab.append(label);
-
- var edit = $('<textarea/>', {
- cols: field.cols,
- rows: field.rows,
- placeholder: field.placeholder
- });
- var val = dup_model.get(
field.name);
- if (val) {
- edit.val(val);
- }
- edit.on('change', function() {
- var o = {};
- o[
field.name] = edit.val();
- dup_model.set(o);
- });
- lcont.remove();
- tab.append(edit);
- tab.attr('colspan', 2);
- }
-
- function wiki_editor(lcont, tab, field) {
- var label = $('<label/>');
- label.text(field.label);
- tab.append(label);
-
- var edit = $('<textarea/>', {
- class: 'wiki shortwiki',
- cols: field.cols,
- rows: field.rows,
- placeholder: field.placeholder
- });
- var val = dup_model.get(
field.name);
- if (val) {
- edit.val(val);
- }
- lcont.remove();
- tab.append(edit);
- var b = $('<button/>', {
- class: 'btn'
- });
- b.html('<i class="icon-pencil"></i> Edit ' + field.label + ' in wiki editor');
- tab.append(b);
- tab.attr('colspan', 2);
-
- dup_model.bind('change:' +
field.name, function () {
- var val = dup_model.get(
field.name);
- edit.val(val ? val : '');
- });
- edit.on('change', function() {
- var o = {};
- o[
field.name] = edit.val();
- dup_model.set(o);
- });
-
- var wiki = new MTrackWikiTextAreaView({
- model: dup_model,
- wikiContext: 'ticket:',
- use_overlay: true,
- Caption: "Edit " + field.label,
- OKLabel: "Accept " + field.label,
- CancelLabel: "Abandon changes to " + field.label,
- srcattr:
field.name,
- renderedattr:
field.name + '_html'
- });
- wiki.bind('editstart', function () {
- in_wiki = true;
- setTimeout(function () {
- dlg.modal('hide');
- }, 1000);
- });
- wiki.bind('editend', function () {
- in_wiki = false;
- dlg.modal('show');
- });
-
- b.click(function () {
- var o = {};
- o[
field.name] = edit.val();
- dup_model.set(o);
- wiki.edit();
- });
- }
-
- function text_editor(lcont, tab, field) {
- lcont.text(field.label);
-
- var inp = $('<input/>', {
- type: "text",
- name:
field.name,
- placeholder: field.placeholder
- });
- var val = view.model.get(
field.name);
- if (val) {
- inp.val(val);
- }
- inp.on('change', function() {
- var o = {};
- o[
field.name] = inp.val();
- dup_model.set(o);
- });
-
- tab.append(inp);
- }
-
- function multiselect_editor(lcont, tab, field) {
- lcont.text(field.label);
-
- var el = $('<div/>');
- tab.append(el);
- var view = new MTrackSelectEditorView({
- el: el,
- model: dup_model,
- multiple: true,
- srcattr:
field.name,
- label: field.label,
- width: '418px',
- values: field.options,
- defval: field["default"],
- placeholder: field.placeholder
- });
- view.render();
- }
-
- function select_editor(lcont, tab, field) {
- lcont.text(field.label);
-
- var el = $('<span/>');
- tab.append(el);
- var view = new MTrackSelectEditorView({
- el: el,
- model: dup_model,
- srcattr:
field.name,
- label: field.label,
- width: '418px',
- values: field.options,
- defval: field["default"],
- placeholder: field.placeholder
- });
- view.render();
- }
-
- function ticketdeps_editor(lcont, tab, field) {
- lcont.text(field.label);
-
- var el = $('<span/>');
- tab.append(el);
- var view = new MTrackTicketDepEditView({
- el: el,
- model: dup_model,
- srcattr:
field.name,
- label: field.label,
- });
- view.render();
- }
-
- function tags_editor(lcont, tab, field) {
- lcont.text(field.label);
-
- var el = $('<span/>');
- tab.append(el);
- var view = new MTrackTagEditView({
- el: el,
- model: dup_model,
- srcattr:
field.name,
- label: field.label,
- });
- view.render();
- }
-
- function cc_editor(lcont, tab, field) {
- lcont.text(field.label);
-
- var el = $('<span/>');
- tab.append(el);
- var view = new MTrackCcEditView({
- el: el,
- model: dup_model,
- srcattr:
field.name,
- label: field.label,
- });
- view.render();
- }
-
- var editors = {
- multi: multi_editor,
- select: select_editor,
- multiselect: multiselect_editor,
- tags: tags_editor,
- cc: cc_editor,
- ticketdeps: ticketdeps_editor,
- text: text_editor,
- wiki: wiki_editor
- };
-
- function add_editor(tr, tab, field)
- {
- if (field.type == 'readonly') return;
-
- var editor = text_editor;
-
- if (field.type in editors) {
- editor = editors[field.type];
- }
- tr = tr.clone();
- var lcont = $('td.fieldname', tr);
- var div = $('td.fieldvalue', tr);
- tab.append(tr);
- editor(lcont, div, field);
- }
-
- function add_tab_and_fields(group) {
- var tab = add_group_tab(group);
-
- var table = $('table', tab);
- var tr = $('tr', table);
- tr.remove();
-
- if (
group.name == 0) {
- // Synthesize some fields
- add_editor(tr, table, {
- name: 'summary',
- label: 'Summary'
- });
- add_editor(tr, table, {
- name: 'status',
- label: 'Status',
- type: 'select',
- options: mtrack_ticket_states
- });
- if (dup_model.get('status') != 'closed') {
- add_editor(tr, table, {
- name: 'resolution',
- label: 'Resolution',
- placeholder: 'Resolve ticket as...',
- type: 'select',
- options: mtrack_resolutions
- });
- }
- }
- _.each(group.fields, function (field) {
- add_editor(tr, table, field);
- });
- }
-
- var com_tab = add_tab_and_fields({
- name: 'Comment',
- fields: [
- {
- name: 'comment',
- label: 'Comment',
- type: 'wiki',
- placeholder: 'Something on your mind? Share it here!',
- rows: 10,
- cols: 78
- }
- ]
- });
-
- /* Create a tab for each category of field */
- for (var gidx in this.options.fields) {
- add_tab_and_fields(this.options.fields[gidx]);
- }
- tab_ul.append(tab_att);
-
- /* handle uploads */
- var uploading = false;
- $('#confirm-upload', dlg).click(function () {
- uploading = true;
- $('#upload-form', dlg).submit();
- });
- $('#upload_target', dlg).on('load', function () {
- var res = $(this).contents().find('body').text();
- try {
- res = JSON.parse(res);
- if (res.status == 'success') {
- if (uploading) {
- $('<div class="alert alert-success">' +
- '<a class="close" data-dismiss="alert">×</a>' +
- 'Upload successful</div>').
- appendTo($('#tkt-edit-attachments', dlg));
- dup_model.getAttachments().reset(res.attachments);
- }
- $('input[type=file]', dlg).val('');
- } else {
- $('<div class="alert alert-danger">' +
- '<a class="close" data-dismiss="alert">×</a>' +
- res.message + '</div>').
- appendTo($('#tkt-edit-attachments', dlg));
- }
- } catch (e) {
- }
- uploading = false;
- });
-
- // Present the conflict resolution UI.
- // This is an alternative form that shows the changes side-by-side
- // and allows the user to pick a resolution:
- // - Accept my changes
- // - Accept their changes
- // - Cancel
- // The first two will re-display the edit dialog with the updated
- // model, the latter will cancel the edit dialog.
- function show_conflict_resolver(conflict) {
- var updated = conflict.updated;
- delete conflict.updated;
- delete conflict.description_html;
- var o = {
- nsident: dup_model.get('nsident'),
- summary: dup_model.get('summary'),
- conflict: conflict,
- updated: updated,
- ABSWEB: ABSWEB
- };
- var CD = $(_.template(mtrack_underscore_templates['ticket-conflict'], o));
- $('body').append(CD);
- $('.timeinterval', CD).timeago();
-
- // Fixup model so that we don't trigger a 409 on next save
- // (unless there is a further conflict!)
- dup_model.set({updated: o.updated});
-
- dlg.modal('hide');
- CD.modal('show');
- CD.on('hidden', function() {
- CD.remove();
- });
-
- $('button.mine', CD).click(function () {
- var editor = new TE({
- model: dup_model,
- fields: view.options.fields
- });
- CD.modal('hide');
- editor.show(on_success);
- });
-
- $('button.theirs', CD).click(function () {
- var o = {};
- for (var k in conflict) {
- var item = conflict[k];
- o[k] = item[1];
- }
- dup_model.set(o);
-
- var editor = new TE({
- model: dup_model,
- fields: view.options.fields
- });
- CD.modal('hide');
- editor.show(on_success);
- });
- }
- /*
- $('.modal-footer button.conflict', dlg).click(function () {
- show_conflict_resolver({
- updated: {
- who: 'otherguy',
- when: "2012-04-11T14:54:36+00:00",
- cid: "17"
- },
- summary: ['my lemons', 'your lemons']
- });
- });
- */
-
- // Save. We want to apply the attributes from the dup_model to
- // the real model.
- $('.modal-footer button.btn-primary', dlg).click(function () {
- view.model.save(dup_model.attributes, {
- success: function(model) {
- if ('success' in on_success) {
- on_success.success(model);
- }
- dlg.modal('hide');
- },
- error: function (model, resp) {
- // If a conflict was detected, show some useful UI to help
- // them through it
- var is_conflict = false;
-
- if (_.isObject(resp)) {
- try {
- var r = JSON.parse(resp.responseText);
- if (r.code == 409) {
- is_conflict = r.extra;
- }
- } catch (e) {
- }
- }
-
- if (!is_conflict) {
- mtrack_ajax_error_to_dom(resp, $('div.modal-header', dlg));
- return;
- }
-
- show_conflict_resolver(is_conflict);
- }
- });
- });
-
- dlg.modal('show');
- }
-});
-
-// Ticket viewer
-var TV = Backbone.View.extend({
- render: function() {
- var t = _.template(mtrack_underscore_templates['ticket-show']);
- var o = this.model.toJSON();
- $(this.el).html(t(o));
-// $('body').attr('data-target', '#tkt-nav');
-// $('body').scrollspy({offset: 30});
-
- var F = $('#tkt-fields');
- // Table row template
- var TR = F.find('tr');
- var table = TR.parent();
- TR.remove();
-
- var model = this.model;
- var view = this;
-
- var attach_list = $('#attach-list', this.el);
- function redraw_attachment_list() {
- attach_list.empty();
- var t = _.template(mtrack_underscore_templates['attachment-item']);
- var count = 0;
-
- model.getAttachments().each(function (att) {
- count++;
- var o = att.toJSON();
- if ('width' in o) {
- o.image = true;
- } else {
- o.image = false;
- }
- var d = $(t(o));
- d.data('attachment-model', att);
- attach_list.append(d);
- });
- if (count) {
- $('.timeinterval', attach_list).timeago();
- $('#attach', view.el).show();
- $('.tkt-outline a[href=#attach]').show();
- } else {
- $('#attach', view.el).hide();
- $('.tkt-outline a[href=#attach]').hide();
- }
- }
- redraw_attachment_list();
- model.getAttachments().bind('all', function () {
- redraw_attachment_list();
- });
-
- var change_tpl = _.template(
- mtrack_underscore_templates['ticket-event-show']);
- var change_cont = $('#tkt-comments', this.el);
-
- function add_one_change(cs) {
- var d = $('<div/>');
- $(d).html(change_tpl(cs.toJSON()));
- var had_comment = false;
- var commit = false;
- _.each(cs.get('audit'), function (a) {
- if (a.label == 'Comment') {
- had_comment = true;
- if (a.value.match(/^\(In /)) {
- commit = true;
- }
- }
- });
- if (had_comment) {
- d.addClass('chg-comment');
- if (commit) {
- d.addClass('chg-commit');
- }
- } else {
- d.addClass('chg-no-comment');
- }
- d.appendTo(change_cont);
- $('.toggle-desc', d).click(function () {
- $('#' + $(this).attr('desc-id')).toggle();
- return false;
- });
- $('.timeinterval', d).timeago();
- }
- function redraw_change_list() {
- model.getChanges().each(function (cs) {
- add_one_change(cs);
- });
- }
- redraw_change_list();
-
- function refresh_changes() {
- var changes = model.getChanges();
- var recent =
changes.at(0);
-
- // If a modal dialog is open, don't do any updating
- if ($('body').hasClass('modal-open')) {
- return;
- }
-
- if (!recent) {
- return;
- }
-
- changes.fetch({
- success: function (c, r) {
- if (!r.length) {
- return;
- }
- var latest =
changes.at(0);
- if (
latest.id ==
recent.id) {
- // No change
- return;
- }
- model.getAttachments().fetch();
- var base_cid = model.get('updated').cid;
- model.fetch({
- success: function (model, resp) {
- if (model.get('updated').cid == base_cid) {
- return;
- }
- /* it changed */
- model.unset('comment', {silent: true});
- view.render();
- }
- });
- }
- });
- }
- view.refresh = refresh_changes;
-
- var user_template = _.template(mtrack_underscore_templates['user-name']);
- function render_user(user) {
- if (!_.isObject(user)) {
- user = {
- id: user,
- label: user
- };
- }
- var o = _.clone(user);
- o.ABSWEB = ABSWEB;
- return user_template(o);
- }
-
- function render_user_list(users) {
- var res = [];
- _.each(users, function(user) {
- res.push(render_user(user));
- });
- return res.join(' ');
- }
-
- function render_change_time(item) {
- item = _.clone(item);
- item.ABSWEB = ABSWEB;
- return _.template(mtrack_underscore_templates['item-changed'], item);
- }
-
- var renderers = {
- 'owner': render_user,
- 'cc': render_user_list,
- 'created': render_change_time,
- 'updated': render_change_time,
- };
-
- function process_field(field) {
- if (
field.name == 'description') {
- return;
- }
- var val = model.get(
field.name);
- if (typeof(val) == 'undefined' || val == null) {
- return;
- }
- if (_.isArray(val) && val.length == 0) {
- return;
- }
- if (typeof(val) == 'string' && val == '') {
- return;
- }
- var tr = TR.clone();
- $('td.fieldname', tr).text(field.label).attr('title', field.label);
- if (
field.name in renderers) {
- $('td.fieldvalue', tr).html(renderers[
field.name](val));
- } else {
- // If we have a template defined, then use the generic template
- // based renderer. First look to see if we have a template
- // for the specific field name, then try to fall back to a
- // generic formatter by type.
- var tplname = "ticket-field-byname-" +
field.name;
- if (!(tplname in mtrack_underscore_templates)) {
- // Try by type
- tplname = "ticket-field-bytype-" + field.type;
- }
-
- var render = function (a) {
- return $('<div/>').text(a).html();
- };
-
- if (tplname in mtrack_underscore_templates) {
- var t = _.template(mtrack_underscore_templates[tplname]);
- render = function (a) {
- /* wrap it up so that the value itself is accessible,
- * as some of the data types we have encode the ids
- * in the keys of an object and we can't iterate
- * the context without a name in the template handler */
- var o = {
- value: a,
- ABSWEB: ABSWEB
- };
- return t(o);
- };
- }
- $('td.fieldvalue', tr).html(render(val));
- }
- table.append(tr);
- }
-
- if (model.get('status') == 'closed') {
- process_field({name: 'resolution', label: 'Resolved'});
- } else {
- process_field({name: 'status', label: 'Status'});
- }
- if (model.get('parent')) {
- process_field({name: 'parent', label: 'Parent', type: 'ticket'});
- }
-
- process_field({name: 'created', label: 'Opened'});
- if (model.get('updated') &&
- model.get('updated').cid != model.get('created').cid) {
- process_field({name: 'updated', label: 'Updated'});
- }
-
- for (var gidx in this.options.fields) {
- var group = this.options.fields[gidx];
- _.each(group.fields, process_field);
- }
- $('.timeinterval', this.el).timeago();
-
- $('#togglebtn', this.el).click(function () {
- $('#tkt-fields', this.el).toggle();
- return false;
- });
-
- // Open the ticket editor
- $('#editbtn', this.el).click(function () {
- var editor = new TE({
- model: view.model,
- fields: view.options.fields
- });
- editor.show({
- success: function (model) {
- window.location.replace(
- ABSWEB + 'ticket.php/' + model.get('nsident'));
- },
- hidden: function () {
- refresh_changes();
- }
- });
- return false;
- });
-
- /* operates on the result of a ticket split; if saved,
- * we're taken to the ticket page for the newly saved ticket */
- function edit_split(model) {
- var editor = new TE({
- model: model,
- fields: view.options.fields
- });
- editor.show({
- success: function (model) {
- /* go to that ticket page */
- window.location = ABSWEB + 'ticket.php/' + model.get('nsident');
- },
- hidden: function () {
- refresh_changes();
- }
- });
- return false;
- }
-
- function make_split_ticket() {
- var S = view.model.clone();
- console.log("cloned as", S);
- S.unset('spent');
- S.unset('remaining');
- S.unset('estimated');
- S.unset('nsident');
- S.unset('id');
- S.unset('created');
- S.unset('updated');
- S.set({children: []});
- S.set({description:
- "\\nSplit from #" + view.model.get('nsident') + "\\n\\n---\\n\\n" +
- view.model.get('description')});
- // Pointless to clone it in a closed state
- if (S.get('status') == 'closed') {
- S.set({status: 'open'});
- }
- S.unset('resolution');
- return S;
- }
- $('#splitsib', this.el).click(function () {
- edit_split(make_split_ticket());
- return false;
- });
- $('#splitchild', this.el).click(function () {
- var S = make_split_ticket();
- // New ticket is a child of the current one
- S.set({ptid:
view.model.id});
- edit_split(S);
- return false;
- });
-
- if (view.model.isNew()) {
- var editor = new TE({
- model: view.model,
- fields: view.options.fields
- });
- editor.show({
- success: function (model) {
- window.location = ABSWEB + 'ticket.php/' + model.get('nsident');
- },
- hidden: function () {
- refresh_changes();
- }
- });
- }
-
- window.mtrack_reply_comment = function (cid) {
- var c = view.model.getChanges().get(cid);
- orig_comment = view.model.get("comment");
- var comment = orig_comment || '';
- if (comment.length) {
- comment = comment + "\\n\\n";
- }
- var reason = c.get('reason');
- // cite it
- reason = reason.replace(/^(\s*)/mg, "> \$1");
- comment = comment + "Replying to [comment:" + cid + " a comment by " +
- c.get('who') + "]\\n" + reason + "\\n";
- in_reply = true;
- view.model.set({'comment': comment});
-
- $('#editbtn', this.el).trigger('click');
-
- return false;
- }
-
- return this;
- }
-});
$(document).ready(function() {
var TheTicket = null;
@@ -939,7 +73,7 @@
TheTicket.get('summary'));
});
- var V = new TV({
+ var V = new MTrackTicketViewer({
model: TheTicket,
fields: FIELDSET,
el: $('#ticket')
https://bitbucket.org/wez/mtrack/changeset/6a1cb0c55463/
changeset: 6a1cb0c55463
user: wez
date: 2012-04-26 06:06:37
summary: add cache-busting query parameter for css and js bits
affected #: 1 file
diff -r 0aaf38c83cc7e0c21fad6fe293dd4f8698b2fcf2 -r 6a1cb0c5546381146b452d654d9cb02a2cf1c6fa inc/web.php
--- a/inc/web.php
+++ b/inc/web.php
@@ -133,8 +133,8 @@
<meta http-equiv="X-UA-Compatible" content="IE=8"><title>$title</title>
$fav
-<link rel="stylesheet" href="${ABSWEB}css.php" type="text/css" />
-<script language="javascript" type="text/javascript" src="${ABSWEB}js.php"></script>
+<link rel="stylesheet" href="${ABSWEB}css.php?2" type="text/css" />
+<script language="javascript" type="text/javascript" src="${ABSWEB}js.php?2"></script></head><body>
HTML;
https://bitbucket.org/wez/mtrack/changeset/39666ee9767d/
changeset: 39666ee9767d
user: wez
date: 2012-04-26 06:29:43
summary: Don't show the comment box for new tickets.
Re-arrange the ticket field categories for the new editor
affected #: 3 files
diff -r 6a1cb0c5546381146b452d654d9cb02a2cf1c6fa -r 39666ee9767da5d2c0ab299cebdb6ae28169eb10 inc/issue.php
--- a/inc/issue.php
+++ b/inc/issue.php
@@ -2121,6 +2121,7 @@
array(
"description" => array(
"label" => "Full description",
+ "placeholder" => "Describe it here",
"ownrow" => true,
"type" => "wiki",
"rows" => 10,
@@ -2160,20 +2161,9 @@
"label" => "Keywords",
"type" => "tags",
),
- "children" => array(
- "label" => "Children",
- "type" => "ticketdeps",
- ),
- "dependencies" => array(
- "label" => "Depends On",
- "type" => "ticketdeps",
- ),
- "blocks" => array(
- "label" => "Blocks",
- "type" => "ticketdeps",
- ),
"changelog" => array(
- "label" => "ChangeLog (customer visible)",
+ "label" => "ChangeLog",
+ "placeholder" => "customer visible; choose your words wisely!",
"type" => "multi",
"ownrow" => true,
"rows" => 5,
@@ -2211,7 +2201,20 @@
"type" => "text",
"placeholder" => "after logged time"
),
-
+ ),
+ "Dependencies" => array(
+ "children" => array(
+ "label" => "Children",
+ "type" => "ticketdeps",
+ ),
+ "dependencies" => array(
+ "label" => "Depends On",
+ "type" => "ticketdeps",
+ ),
+ "blocks" => array(
+ "label" => "Blocks",
+ "type" => "ticketdeps",
+ ),
),
);
$tkt->augmentFormFields($FIELDSET);
diff -r 6a1cb0c5546381146b452d654d9cb02a2cf1c6fa -r 39666ee9767da5d2c0ab299cebdb6ae28169eb10 web/js/templates/ticket.edit.html
--- a/web/js/templates/ticket.edit.html
+++ b/web/js/templates/ticket.edit.html
@@ -10,6 +10,10 @@
width: 25em;
font-size: 1.2em;
}
+ div.ticketeditor table tr td.fieldvalue textarea.multi {
+ font-size: 1.2em;
+ width: 45em;
+ }
div.ticketeditor table tr td.fieldvalue ul.chzn-choices {
border: solid 1px #d7d7d7;
font-size: 1.2em;
diff -r 6a1cb0c5546381146b452d654d9cb02a2cf1c6fa -r 39666ee9767da5d2c0ab299cebdb6ae28169eb10 web/js/views.js
--- a/web/js/views.js
+++ b/web/js/views.js
@@ -2912,8 +2912,10 @@
var label = $('<label/>');
label.text(field.label);
tab.append(label);
+ tab.append("<br>");
var edit = $('<textarea/>', {
+ class: 'multi',
cols: field.cols,
rows: field.rows,
placeholder: field.placeholder
@@ -2936,6 +2938,7 @@
var label = $('<label/>');
label.text(field.label);
tab.append(label);
+ tab.append("<br>");
var edit = $('<textarea/>', {
class: 'wiki shortwiki',
@@ -3133,7 +3136,8 @@
// Synthesize some fields
add_editor(tr, table, {
name: 'summary',
- label: 'Summary'
+ label: 'Summary',
+ placeholder: 'One line summary -- required!'
});
add_editor(tr, table, {
name: 'status',
@@ -3156,19 +3160,22 @@
});
}
- var com_tab = add_tab_and_fields({
- name: 'Comment',
- fields: [
- {
- name: 'comment',
- label: 'Comment',
- type: 'wiki',
- placeholder: 'Something on your mind? Share it here!',
- rows: 10,
- cols: 78
- }
- ]
- });
+ // Don't show the comment for new tickets!
+ if (!view.model.isNew()) {
+ add_tab_and_fields({
+ name: 'Comment',
+ fields: [
+ {
+ name: 'comment',
+ label: 'Comment',
+ type: 'wiki',
+ placeholder: 'Something on your mind? Share it here!',
+ rows: 10,
+ cols: 78
+ }
+ ]
+ });
+ }
/* Create a tab for each category of field */
for (var gidx in this.options.fields) {
https://bitbucket.org/wez/mtrack/changeset/4feb9f259568/
changeset: 4feb9f259568
user: wez
date: 2012-04-26 06:37:00
summary: comment on the ticket that was split to indicate the new ticket number
affected #: 1 file
diff -r 39666ee9767da5d2c0ab299cebdb6ae28169eb10 -r 4feb9f2595689a4c0c5383d2c3bd69ce91a33a85 web/js/views.js
--- a/web/js/views.js
+++ b/web/js/views.js
@@ -3587,15 +3587,26 @@
/* operates on the result of a ticket split; if saved,
* we're taken to the ticket page for the newly saved ticket */
- function edit_split(model) {
+ function edit_split(model, orig) {
var editor = new MTrackTicketEditor({
model: model,
fields: view.options.fields
});
editor.show({
success: function (model) {
- /* go to that ticket page */
- window.location = ABSWEB + 'ticket.php/' + model.get('nsident');
+ /* also add a comment on the original ticket to show that
+ * it was split */
+ var o = {
+ id:
orig.id,
+ comment: "Split and created ticket #" + model.get('nsident')
+ };
+ var C = new MTrackTicket(o);
+ C.save(o, {
+ success: function () {
+ /* go to that ticket page */
+ window.location = ABSWEB + 'ticket.php/' + model.get('nsident');
+ }
+ });
},
hidden: function () {
refresh_changes();
@@ -3626,14 +3637,14 @@
return S;
}
$('#splitsib', this.el).click(function () {
- edit_split(make_split_ticket());
+ edit_split(make_split_ticket(), view.model);
return false;
});
$('#splitchild', this.el).click(function () {
var S = make_split_ticket();
// New ticket is a child of the current one
S.set({ptid:
view.model.id});
- edit_split(S);
+ edit_split(S, view.model);
return false;
});
https://bitbucket.org/wez/mtrack/changeset/98f4ea62c45b/
changeset: 98f4ea62c45b
user: wez
date: 2012-04-26 14:37:42
summary: add a template for the custom field "user" type.
affected #: 3 files
diff -r 4feb9f2595689a4c0c5383d2c3bd69ce91a33a85 -r 98f4ea62c45bd18f7de3dab1991cd58f58afd0ba inc/customfield.php
--- a/inc/customfield.php
+++ b/inc/customfield.php
@@ -66,6 +66,7 @@
$data = array(
'label' => $this->label,
'type' => $this->type,
+ 'customfieldtype' => $this->type,
);
if (strlen($this->default)) {
diff -r 4feb9f2595689a4c0c5383d2c3bd69ce91a33a85 -r 98f4ea62c45bd18f7de3dab1991cd58f58afd0ba web/js/templates/ticket.field.bytype.user.html
--- /dev/null
+++ b/web/js/templates/ticket.field.bytype.user.html
@@ -0,0 +1,2 @@
+<a href="<%= ABSWEB %>user.php/<%- value %>"
+ ><img class='gravatar' height='24' width=24' src="<%= ABSWEB %>avatar.php?u=<%- value %>&s=24"><%- value %></a>
diff -r 4feb9f2595689a4c0c5383d2c3bd69ce91a33a85 -r 98f4ea62c45bd18f7de3dab1991cd58f58afd0ba web/js/views.js
--- a/web/js/views.js
+++ b/web/js/views.js
@@ -3512,7 +3512,16 @@
var tplname = "ticket-field-byname-" +
field.name;
if (!(tplname in mtrack_underscore_templates)) {
// Try by type
- tplname = "ticket-field-bytype-" + field.type;
+ tplname = null;
+ if (field.customfieldtype) {
+ tplname = "ticket-field-bytype-" + field.customfieldtype;
+ if (!(tplname in mtrack_underscore_templates)) {
+ tplname = null;
+ }
+ }
+ if (!tplname) {
+ tplname = "ticket-field-bytype-" + field.type;
+ }
}
var render = function (a) {
https://bitbucket.org/wez/mtrack/changeset/d7a69993c022/
changeset: d7a69993c022
user: wez
date: 2012-04-26 15:02:50
summary: allow defining a custom field template
affected #: 4 files
diff -r 98f4ea62c45bd18f7de3dab1991cd58f58afd0ba -r d7a69993c022f19eaafcbe7b90398d2489aada91 inc/configuration.php
--- a/inc/configuration.php
+++ b/inc/configuration.php
@@ -40,7 +40,10 @@
}
}
- static function set($section, $option, $value) {
+ static function set($section, $option, $value, $b64 = false) {
+ if ($b64) {
+ $value = base64_encode($value);
+ }
self::$runtime[$section][$option] = $value;
}
@@ -72,12 +75,12 @@
$fp = null;
}
- static function get($section, $option) {
+ static function get($section, $option, $b64 = false) {
self::parseIni();
- return self::_get($section, $option);
+ return self::_get($section, $option, $b64);
}
- static function _get($section, $option) {
+ static function _get($section, $option, $b64 = false) {
$ini = self::$ini;
if (isset(self::$ini[$section][$option])) {
$val = self::$ini[$section][$option];
@@ -87,6 +90,10 @@
return null;
}
+ if ($b64) {
+ return base64_decode($val);
+ }
+
while (preg_match('/@\{([a-zA-Z0-9_]+):([a-zA-Z0-9_]+)\}/', $val, $M)) {
$rep = self::_get($M[1], $M[2]);
$val = str_replace($M[0], $rep, $val);
diff -r 98f4ea62c45bd18f7de3dab1991cd58f58afd0ba -r d7a69993c022f19eaafcbe7b90398d2489aada91 inc/customfield.php
--- a/inc/customfield.php
+++ b/inc/customfield.php
@@ -8,6 +8,9 @@
var $order = 0;
var $default;
var $options;
+ /** underscore compatible template to use when presenting the
+ * custom field value in a read-only fashion */
+ var $template;
function getTypeLabel() {
static $field_types = array(
@@ -44,6 +47,7 @@
$field->order = (int)MTrackConfig::get('ticket.custom', "$name.order");
$field->default = MTrackConfig::get('ticket.custom', "$name.default");
$field->options = MTrackConfig::get('ticket.custom', "$name.options");
+ $field->template = json_decode(MTrackConfig::get('ticket.custom', "$name.template", true));
return $field;
}
@@ -59,6 +63,12 @@
MTrackConfig::set('ticket.custom', "$name.order", (int)$this->order);
MTrackConfig::set('ticket.custom', "$name.default", $this->default);
MTrackConfig::set('ticket.custom', "$name.options", $this->options);
+ if ($this->template) {
+ MTrackConfig::set('ticket.custom', "$name.template",
+ json_encode($this->template), true);
+ } else {
+ MTrackConfig::set('ticket.custom', "$name.template", '');
+ }
}
function ticketData(MTrackIssue $issue = null) {
@@ -68,6 +78,9 @@
'type' => $this->type,
'customfieldtype' => $this->type,
);
+ if (strlen($this->template)) {
+ $data['customtemplate'] = $this->template;
+ }
if (strlen($this->default)) {
$data['default'] = $this->default;
diff -r 98f4ea62c45bd18f7de3dab1991cd58f58afd0ba -r d7a69993c022f19eaafcbe7b90398d2489aada91 web/admin/customfield.php
--- a/web/admin/customfield.php
+++ b/web/admin/customfield.php
@@ -12,6 +12,7 @@
$type = $_POST['type'];
$group = $_POST['group'];
$label = $_POST['label'];
+ $template = $_POST['template'];
$options = $_POST['options'];
$default = $_POST['default'];
$order = (int)$_POST['order'];
@@ -36,6 +37,7 @@
$field->order = $order;
$field->options = $options;
$field->default = $default;
+ $field->template = $template;
}
$C->save();
@@ -71,6 +73,7 @@
$group = htmlentities($field->group, ENT_QUOTES, 'utf-8');
$options = htmlentities($field->options, ENT_QUOTES, 'utf-8');
$default = htmlentities($field->default, ENT_QUOTES, 'utf-8');
+ $template = htmlentities($field->template, ENT_QUOTES, 'utf-8');
$order = $field->order;
?><form method='post' id='editfield'>
@@ -121,6 +124,12 @@
then they are ordered by name</em></td></tr>
+ <tr>
+ <td><label for='template'>Template</label></td>
+ <td><textarea name='template' rows='10' cols='50'><?php echo $template ; ?></textarea><br>
+ <em>Optional custom <a href="
http://documentcloud.github.com/underscore/#template">Underscore.js template</a> to use when presenting this field in a read-only manner</em>
+ </td>
+ </tr></table><button type='submit'>Save</button><button type='submit' name='cancel'>Cancel</button>
diff -r 98f4ea62c45bd18f7de3dab1991cd58f58afd0ba -r d7a69993c022f19eaafcbe7b90398d2489aada91 web/js/views.js
--- a/web/js/views.js
+++ b/web/js/views.js
@@ -3504,6 +3504,13 @@
$('td.fieldname', tr).text(field.label).attr('title', field.label);
if (
field.name in renderers) {
$('td.fieldvalue', tr).html(renderers[
field.name](val));
+ } else if (field.customtemplate) {
+ var o = {
+ value: val,
+ ABSWEB: ABSWEB
+ };
+ console.log("using customtemplate", field.customtemplate, o);
+ $('td.fieldvalue', tr).html(_.template(field.customtemplate, o));
} else {
// If we have a template defined, then use the generic template
// based renderer. First look to see if we have a template
https://bitbucket.org/wez/mtrack/changeset/895652387811/
changeset: 895652387811
user: wez
date: 2012-04-26 15:04:17
summary: tidy up a bit
affected #: 2 files
diff -r d7a69993c022f19eaafcbe7b90398d2489aada91 -r 89565238781185e507b86325c1391b4ef8cdf8e6 web/admin/customfield.php
--- a/web/admin/customfield.php
+++ b/web/admin/customfield.php
@@ -127,7 +127,7 @@
<tr><td><label for='template'>Template</label></td><td><textarea name='template' rows='10' cols='50'><?php echo $template ; ?></textarea><br>
- <em>Optional custom <a href="
http://documentcloud.github.com/underscore/#template">Underscore.js template</a> to use when presenting this field in a read-only manner</em>
+ <em>Optional custom <a href="
http://documentcloud.github.com/underscore/#template">Underscore.js template</a> to use when presenting this field in a read-only manner. The value being rendered is made available to the template in the "value" variable.</em></td></tr></table>
diff -r d7a69993c022f19eaafcbe7b90398d2489aada91 -r 89565238781185e507b86325c1391b4ef8cdf8e6 web/js/views.js
--- a/web/js/views.js
+++ b/web/js/views.js
@@ -3509,7 +3509,6 @@
value: val,
ABSWEB: ABSWEB
};
- console.log("using customtemplate", field.customtemplate, o);
$('td.fieldvalue', tr).html(_.template(field.customtemplate, o));
} else {
// If we have a template defined, then use the generic template
https://bitbucket.org/wez/mtrack/changeset/c5a18781c6e0/
changeset: c5a18781c6e0
user: wez
date: 2012-04-26 15:17:36
summary: fixup multiselect template for custom fields that are multiselect
affected #: 1 file
diff -r 89565238781185e507b86325c1391b4ef8cdf8e6 -r c5a18781c6e0d09555fa41acbc25baed20d23fd8 web/js/templates/ticket.field.bytype.multiselect.html
--- a/web/js/templates/ticket.field.bytype.multiselect.html
+++ b/web/js/templates/ticket.field.bytype.multiselect.html
@@ -1,4 +1,13 @@
-<% _.each(value, function (label, id) { %>
- <%- label %>
-<% }); %>
-
+<%
+_.each(value, function (label, id) {
+ if (_.isObject(label) && label.label) {
+%>
+ <%- label.label %>
+<%
+ } else {
+%>
+ <%- label %>
+<%
+ }
+});
+%>
https://bitbucket.org/wez/mtrack/changeset/01f189fc0ee0/
changeset: 01f189fc0ee0
user: wez
date: 2012-04-26 15:22:44
summary: bump cache buster
affected #: 1 file
diff -r c5a18781c6e0d09555fa41acbc25baed20d23fd8 -r 01f189fc0ee031517f6177a26c68f73dab1407e7 inc/web.php
--- a/inc/web.php
+++ b/inc/web.php
@@ -134,7 +134,7 @@
<title>$title</title>
$fav
<link rel="stylesheet" href="${ABSWEB}css.php?2" type="text/css" />
-<script language="javascript" type="text/javascript" src="${ABSWEB}js.php?2"></script>
+<script language="javascript" type="text/javascript" src="${ABSWEB}js.php?3"></script></head><body>
HTML;
https://bitbucket.org/wez/mtrack/changeset/f0c58751157d/
changeset: f0c58751157d
user: wez
date: 2012-04-27 04:32:53
summary: centralize quick-link expansion and make that available to the live search box
affected #: 2 files
diff -r 01f189fc0ee031517f6177a26c68f73dab1407e7 -r f0c58751157db2e19cc4944eadc8f285a4962338 inc/search.php
--- a/inc/search.php
+++ b/inc/search.php
@@ -112,12 +112,57 @@
return self::getEngine()->search($query);
}
+ static function expand_quick_link($q) {
+ global $ABSWEB;
+
+ if (preg_match("/^help$/i", $q)) {
+ return array("Help on $q", $ABSWEB . 'help.php');
+ }
+ if (preg_match('/^#([a-zA-Z0-9]+)$/', $q, $M)) {
+ /* ticket */
+ $t = $M[1];
+ $url = $ABSWEB . "ticket.php/$t";
+ return array("<a href='$url' class='ticketlink'>Ticket #$t</a>", $url);
+ }
+ if (preg_match('/^([0-9]+)$/', $q, $M)) {
+ $t = $M[1];
+ $url = $ABSWEB . "ticket.php/$t";
+ return array("<a href='$url' class='ticketlink'>Ticket #$t</a>", $url);
+ }
+ if (preg_match('/^(?:#?[0-9-]+\s*)+$/', $q)) {
+ /* tickets; show a custom query for those */
+ $tkts = array();
+ foreach (preg_split("/\s+/", $q) as $id) {
+ if ($id[0] == '#') $id = substr($id, 1);
+ $tkts[] = $id;
+ }
+ return array("Ticket query for $q",
+ $ABSWEB . "query.php?ticket=" . join('|', $tkts));
+ }
+ if (preg_match('/^r([a-zA-Z]*\d+)$/', $q, $M)) {
+ /* changeset */
+ $url = mtrack_changeset_url($M[1]);
+ return array("Show changeset $q", $url);
+ }
+ if (preg_match('/^\[([a-zA-Z]*\d+)\]$/', $q, $M)) {
+ /* changeset */
+ $url = mtrack_changeset_url($M[1]);
+ return array("Show changeset $q", $url);
+ }
+ if (preg_match('/^\{(\d+)\}$/', $q, $M)) {
+ /* report */
+ return array("Go to report $q",
+ $ABSWEB . "report.php/$M[1]");
+ }
+ return null;
+ }
+
static function rest_query_array($method, $uri, $captures) {
$q = MTrackAPI::getParam('q');
+ MTrackAPI::checkAllowed($method, 'GET');
/* full text. We hide closed tickets to reduce noise;
* we're more likely to be searching for active items here */
- MTrackAPI::checkAllowed($method, 'GET');
$notickets = "$q -status:closed";
$res = mtrack_cache(array('MTrackSearchDB', '_do_search'),
array($notickets), 6);
@@ -158,6 +203,18 @@
$res[] = $o;
}
}
+
+ $quick = self::expand_quick_link($q);
+ if ($quick) {
+ /* prepend the quick link version */
+ $o = new stdclass;
+ $o->link = $quick[0];
+ $o->url = $quick[1];
+
+ array_unshift($res, $o);
+ }
+
+
/* catch all: take them to the main search page.
* This is here because there are some quick search cases we don't
* handle here, and they might want to see the help on searching */
diff -r 01f189fc0ee031517f6177a26c68f73dab1407e7 -r f0c58751157db2e19cc4944eadc8f285a4962338 web/search.php
--- a/web/search.php
+++ b/web/search.php
@@ -4,50 +4,12 @@
$q = $_GET['q'];
-if (preg_match("/^help$/i", $q)) {
- header("Location: {$ABSWEB}help.php");
+$quick = MTrackSearchDB::expand_quick_link($q);
+if ($quick) {
+ header("Location: " . $quick[1]);
exit;
}
-if (preg_match('/^#([a-zA-Z0-9]+)$/', $q, $M)) {
- /* ticket */
- header("Location: {$ABSWEB}ticket.php/$M[1]");
- exit;
-}
-if (preg_match('/^([0-9]+)$/', $q, $M)) {
- /* ticket */
- header("Location: {$ABSWEB}ticket.php/$M[1]");
- exit;
-}
-
-if (preg_match('/^(?:#?[0-9-]+\s*)+$/', $q)) {
- /* tickets; show a custom query for those */
- $tkts = array();
- foreach (preg_split("/\s+/", $q) as $id) {
- if ($id[0] == '#') $id = substr($id, 1);
- $tkts[] = $id;
- }
- header("Location: {$ABSWEB}query.php?ticket=" . join('|', $tkts));
- exit;
-}
-
-if (preg_match('/^r([a-zA-Z]*\d+)$/', $q, $M)) {
- /* changeset */
- $url = mtrack_changeset_url($M[1]);
- header("Location: $url");
- exit;
-}
-if (preg_match('/^\[([a-zA-Z]*\d+)\]$/', $q, $M)) {
- /* changeset */
- $url = mtrack_changeset_url($M[1]);
- header("Location: $url");
- exit;
-}
-if (preg_match('/^\{(\d+)\}$/', $q, $M)) {
- /* report */
- header("Location: {$ABSWEB}report.php/$M[1]");
- exit;
-}
mtrack_head("Search");
?><h1>Search</h1>
https://bitbucket.org/wez/mtrack/changeset/f12c07a833ce/
changeset: f12c07a833ce
user: postwait
date: 2012-04-15 16:03:36
summary: support google analytics via config [core] google_analytics
affected #: 2 files
diff -r fdd128d6e8245246b4dab7a1582589f54b7137de -r f12c07a833cedd7d81d6e94e37f40551cdfcea0f web/js.php
--- a/web/js.php
+++ b/web/js.php
@@ -36,6 +36,10 @@
);
echo "var ABSWEB = '$ABSWEB';\n";
+$gaq_code = MTrackConfig::get('core', 'google_analytics');
+if($gaq_code) {
+ echo "var _gaq_code = '$gaq_code';\n";
+}
/* defaults for ticket fields */
$tktdefs = new stdclass;
$tktdefs->classification =
diff -r fdd128d6e8245246b4dab7a1582589f54b7137de -r f12c07a833cedd7d81d6e94e37f40551cdfcea0f web/js/mtrack.js
--- a/web/js/mtrack.js
+++ b/web/js/mtrack.js
@@ -374,4 +374,17 @@
});
});
+
+if(typeof _gaq_code != 'undefined') {
+var _gaq = _gaq || [];
+_gaq.push(['_setAccount', _gaq_code]);
+_gaq.push(['_trackPageview']);
+
+(function() {
+ var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
+ ga.src = ('https:' == document.location.protocol ? '
https://ssl' : '
http://www') + '.
google-analytics.com/ga.js';
+ var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
+})();
+}
+
// vim:ts=2:sw=2:et:
https://bitbucket.org/wez/mtrack/changeset/a694d564da23/
changeset: a694d564da23
user: postwait
date: 2012-04-23 15:37:52
summary: support google analytics
affected #: 2 files
diff -r 230c7682b57252b004d1beb68e3999e62583a8c7 -r a694d564da23729c5ecd0c816e27972544896a74 web/js.php
--- a/web/js.php
+++ b/web/js.php
@@ -38,6 +38,10 @@
);
echo "var ABSWEB = '$ABSWEB';\n";
+$gaq_code = MTrackConfig::get('core', 'google_analytics');
+if($gaq_code) {
+ echo "var _gaq_code = '$gaq_code';\n";
+}
/* defaults for ticket fields */
$tktdefs = new stdclass;
$tktdefs->classification =
diff -r 230c7682b57252b004d1beb68e3999e62583a8c7 -r a694d564da23729c5ecd0c816e27972544896a74 web/js/mtrack.js
--- a/web/js/mtrack.js
+++ b/web/js/mtrack.js
@@ -500,4 +500,17 @@
});
});
+
+if(typeof _gaq_code != 'undefined') {
+var _gaq = _gaq || [];
+_gaq.push(['_setAccount', _gaq_code]);
+_gaq.push(['_trackPageview']);
+
+(function() {
+ var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
+ ga.src = ('https:' == document.location.protocol ? '
https://ssl' : '
http://www') + '.
google-analytics.com/ga.js';
+ var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
+})();
+}
+
// vim:ts=2:sw=2:et:
https://bitbucket.org/wez/mtrack/changeset/e2fe19238dfe/
changeset: e2fe19238dfe
user: wez
date: 2012-04-27 04:42:48
summary: close out this stray head
affected #: 0 files
https://bitbucket.org/wez/mtrack/changeset/39cdfd6e69d9/
changeset: 39cdfd6e69d9
user: wez
date: 2012-04-27 04:48:25
summary: merge google analytics bits from Theo
affected #: 2 files
diff -r f0c58751157db2e19cc4944eadc8f285a4962338 -r 39cdfd6e69d98124ad6888abce9ddbaf24822e27 web/js.php
--- a/web/js.php
+++ b/web/js.php
@@ -38,6 +38,10 @@
);
echo "var ABSWEB = '$ABSWEB';\n";
+$gaq_code = MTrackConfig::get('core', 'google_analytics');
+if($gaq_code) {
+ echo "var _gaq_code = '$gaq_code';\n";
+}
/* defaults for ticket fields */
$tktdefs = new stdclass;
$tktdefs->classification =
diff -r f0c58751157db2e19cc4944eadc8f285a4962338 -r 39cdfd6e69d98124ad6888abce9ddbaf24822e27 web/js/mtrack.js
--- a/web/js/mtrack.js
+++ b/web/js/mtrack.js
@@ -500,4 +500,17 @@
});
});
+
+if(typeof _gaq_code != 'undefined') {
+var _gaq = _gaq || [];
+_gaq.push(['_setAccount', _gaq_code]);
+_gaq.push(['_trackPageview']);
+
+(function() {
+ var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
+ ga.src = ('https:' == document.location.protocol ? '
https://ssl' : '
http://www') + '.
google-analytics.com/ga.js';
+ var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
+})();
+}
+
// vim:ts=2:sw=2:et:
https://bitbucket.org/wez/mtrack/changeset/f8c10f33101b/
changeset: f8c10f33101b
user: wez
date: 2012-04-27 04:50:14
summary: remove redundant ticket list match from live search
affected #: 1 file
diff -r 39cdfd6e69d98124ad6888abce9ddbaf24822e27 -r f8c10f33101bbc084406ff463204493f61db0779 inc/search.php
--- a/inc/search.php
+++ b/inc/search.php
@@ -136,7 +136,7 @@
if ($id[0] == '#') $id = substr($id, 1);
$tkts[] = $id;
}
- return array("Ticket query for $q",
+ return array("Show ticket list: $q",
$ABSWEB . "query.php?ticket=" . join('|', $tkts));
}
if (preg_match('/^r([a-zA-Z]*\d+)$/', $q, $M)) {
@@ -195,13 +195,6 @@
$o->link = "<a class='ticketlink' href='$o->url'>#$r->nsident $r->summary</a>";
$res[] = $o;
}
- if (preg_match("/[ -]/", $q)) {
- /* smells like a ticket list */
- $o = new stdclass;
- $o->link = "<em>Show a ticket list containing $q</em>";
- $o->url = $GLOBALS['ABSWEB'] . "search.php?q=" . urlencode($q);
- $res[] = $o;
- }
}
$quick = self::expand_quick_link($q);
Repository URL:
https://bitbucket.org/wez/mtrack/
--
This is a commit notification from
bitbucket.org. You are receiving
this because you have the service enabled, addressing the recipient of
this email.