Integration of validation results into the file/diff viewer by kmaglione (bug 663254)

This commit is contained in:
Kris Maglione 2011-07-30 16:35:12 -04:00 коммит произвёл Andy McKay
Родитель 90f1fcf912
Коммит 132d5fdc56
8 изменённых файлов: 422 добавлений и 80 удалений

Просмотреть файл

@ -1,24 +1,30 @@
<div id="content-wrapper">
<div>
<div id="diff-wrapper">
<div class="diff-bar js-hidden"></div>
{% if viewer %}
{% if viewer.selected %}
{% if not viewer.selected['binary'] %}
<pre id="content" class="brush: {{ viewer.selected['syntax'] }}; toolbar: false">{{ content }}</pre>
{% endif %}
{% endif %}
{% endif %}
{% if diff %}
{% if left or right %}
<pre id="diff" class="diff-bar-height js-hidden brush: diff; toolbar: false"></pre>
<pre class="left js-hidden">{{ left }}</pre>
<pre class="right js-hidden">{{ right }}</pre>
{% endif %}
{% endif %}
</div>
{% if viewer %}
{% if viewer.selected %}
{% if not viewer.selected['binary'] %}
<pre id="content" class="brush: {{ viewer.selected['syntax'] }}; toolbar: false">{{ content }}</pre>
{% endif %}
{% with selected = viewer.selected %}
{% include "files/file.html" %}
{% endwith %}
{% endif %}
{% endif %}
{% if diff %}
<div id="diff-wrapper">
{% if left or right %}
<div class="diff-bar js-hidden"></div>
<pre id="diff" class="diff-bar-height js-hidden brush: diff; toolbar: false"></pre>
<pre class="left js-hidden">{{ left }}</pre>
<pre class="right js-hidden">{{ right }}</pre>
{% endif %}
</div>
{% if diff.left and diff.left.selected %}
{% with selected = diff.left.selected %}
{% include "files/file.html" %}

Просмотреть файл

@ -3,5 +3,5 @@
{% if value['filename'] != value['truncated'] %}title="{{ value['filename'] }}"{% endif %}
data-delay="0"
href="{{ value['url'] }}"
data-short="{{ value['short'] }}">{{ value['truncated'] }}</a>
data-short="{{ value['short'] }}"><span>{{ value['truncated'] }}</span></a>
</li>

Просмотреть файл

@ -11,7 +11,9 @@
<div id="metadata" class="js-hidden"
data-name="{{ addon.name }}"
data-slug="{{ addon.slug }}"
data-version="{{ version }}"></div>
data-version="{{ version }}"
data-validate-url="{{ validate_url }}"
data-validation-failed="{{ _("Validation failed:") }}"></div>
<h3>
<a href="{{ file_link['url'] }}">{{ addon.name }} {{ version }}</a>
@ -29,6 +31,9 @@
</p>
<div class="notification-box error js-hidden"></div>
{% endif %}
<div id="validating" class="js-hidden">
<span class="waiting">{{ _('Fetching validation results...') }}</span>
</div>
<div id="files">
{% if files %}
<h4>{{ _('Files:') }}</h4>
@ -46,10 +51,10 @@
<table id="commands">
<tr><th><code>k</code></th> <td><a href="#" id="files-up">{{ _('Previous file') }}</a></td></tr>
<tr><th><code>j</code></th> <td><a href="#" id="files-down">{{ _('Next file') }}</a></td></tr>
{% if diff %}
<tr><th><code>[c</code></th> <td><a href="#" id="files-change-prev">{{ _('Previous change') }}</a></td></tr>
<tr><th><code>]c</code></th> <td><a href="#" id="files-change-next">{{ _('Next change') }}</a></td></tr>
{% endif %}
{% if diff %}
<tr><th><code>[c</code></th> <td><a href="#" id="files-change-prev">{{ _('Previous change') }}</a></td></tr>
<tr><th><code>]c</code></th> <td><a href="#" id="files-change-next">{{ _('Next change') }}</a></td></tr>
{% endif %}
<tr><th><code>e</code></th> <td><a href="#" id="files-expand-all">{{ _('Expand all') }}</a></td></tr>
<tr><th><code>h</code></th> <td><a href="#" id="files-hide">{{ _('Hide or unhide tree') }}</a></td></tr>
<tr><th><code>w</code></th> <td><a href="#" id="files-wrap">{{ _('Wrap or unwrap text') }}</a></td></tr>

Просмотреть файл

@ -26,7 +26,16 @@ def setup_viewer(request, file_obj):
'version': file_obj.version,
'addon': file_obj.version.addon,
'status': False,
'selected': {}}
'selected': {},
'validate_url': ''}
if (acl.action_allowed(request, 'Editors', '%') or
acl.check_addon_ownership(request, file_obj.version.addon,
viewer=True, ignore_disabled=True)):
data['validate_url'] = reverse('devhub.json_file_validation',
args=[file_obj.version.addon.slug,
file_obj.id])
if acl.action_allowed(request, 'Editors', '%'):
data['file_link'] = {'text': _('Back to review'),

Просмотреть файл

@ -10,7 +10,7 @@
#files li a {
display: inline-block;
padding: 2px 20px;
padding: 1px 1px 2px 20px;
}
#files ul.root {
@ -45,6 +45,11 @@ body.file-viewer div.section {
padding-left: 0;
}
#validating {
text-align: center;
margin-bottom: 1em;
}
span.number {
width: 4em;
display: inline-block;
@ -84,18 +89,75 @@ a.open {
background: url('../../img/icons/minus.gif') 0 no-repeat;
}
a.diff {
#files a > span {
-moz-boder-radius: 4px;
border-radius: 4px;
padding: 3px;
}
a.diff > span {
background-color: #ffffcc;
}
a.known > span {
color: #bbb;
}
#files a.notice > span, #files a.warning > span, #files a.error > span {
margin: -1px;
}
#files a.notice > span {
border: 1px dotted blue;
}
#files a.warning.warning > span {
border: 1px dotted red;
}
#files a.error.error.error > span {
border: 1px solid red;
}
a.selected > span {
background-color: #ddf8dd;
}
.gutter a.notice, .gutter a.warning, .gutter a.error {
margin: -1px;
-moz-border-radius: 3px;
border-radius: 3px;
border: 1px solid black;
}
.code .notice, .code .warning, .code .error {
margin: -1px;
}
.gutter a.notice, .diff-bar .notice.notice {
background-color: yellow;
}
.code .notice {
border: 1px dotted yellow;
}
.gutter a.warning.warning, .diff-bar .warning.warning.warning {
background-color: orange;
}
.code .warning.warning {
border: 1px dotted orange;
}
.gutter a.error.error.error, .diff-bar .error.error.error.error {
background-color: red;
}
.code .error.error.error {
border: 1px dotted red;
}
div.diff-bar {
margin-left: -1px;
}
a.selected {
background-color: #ddf8dd;
}
.waiting {
background-image: url(../../img/zamboni/loading-white.gif);
background-repeat: no-repeat;
@ -123,7 +185,7 @@ a.selected {
padding: 0 0 0 1ex;
}
#diff {
#diff, #content {
padding-left: 11px;
}
@ -140,7 +202,6 @@ td.gutter, td.code {
border: 1px solid gray;
width: 10px;
position: absolute;
overflow: hidden;
height: 200px;
}
@ -174,6 +235,89 @@ td.gutter, td.code {
max-width: 100%;
}
#tooltip > span {
white-space: pre-wrap;
text-align: left;
max-width: 70ex;
}
.message {
display:none;
position: absolute;
padding-left: 10px;
top: -.2em;
left: 100%;
position: absolute;
z-index: 16385;
text-align: left;
font-size: 11px;
}
.diff-bar .message {
top: -.8em;
margin-top: -4px;
}
.message-inner {
position: relative;
padding: 0.5em 1em;
border: 1px solid #fff;
border-radius: .8em;
background: #2A4364;
color: white;
border-radius: .8em;
-moz-border-radius: .8em;
-webkit-border-radius: .8em;
}
.message a {
color: white;
font-weight: bold;
text-decoration: underline !important;
}
.message-inner > div {
line-height: 1.2em;
width: 70ex;
white-space: pre-wrap;
}
.message-inner > div + div {
margin-top: 1em;
}
.message p {
margin: 1em 0;
}
.message-inner::before {
content: "\00a0";
display: block; /* reduce the damage in FF3.0 */
position: absolute;
width: 0;
height: 0;
margin-top: -6px;
top: 1.1em;
left: -16px;
border: solid transparent;
border-width: 6px 9px;
border-right-color: #2A4364;
pointer-events: none;
}
.message-container {
position: relative;
overflow: visible !important;
}
.message-container:hover > .message {
display: block;
}
.number-combo {
display: inline-block;
}

Просмотреть файл

@ -942,7 +942,7 @@ div.popup-shim {
line-height: 1.2em;
}
#tooltip:before {
#tooltip::before {
content: "\00a0";
display: block; /* reduce the damage in FF3.0 */
position: absolute;
@ -956,7 +956,7 @@ div.popup-shim {
border-top-color: #2A4364;
pointer-events: none;
}
#tooltip.error:before {
#tooltip.error::before {
border-top-color: #6c1a1a;
}

Просмотреть файл

@ -72,7 +72,7 @@ if (typeof SyntaxHighlighter !== 'undefined') {
return html;
};
SyntaxHighlighter.Highlighter.prototype.getLineHtml = function(lineIndex, lineNumber, code) {
SyntaxHighlighter.Highlighter.prototype.getLineHtml = function(lineIndex, lineNumber, code) {
var classes = [
'original',
'line',
@ -91,8 +91,11 @@ if (typeof SyntaxHighlighter !== 'undefined') {
/* HTML parsing with regex warning disclaimer. This lib writes out
* well formed lines with <code> and <a>. We want a hint
* of the line length without all the syntax highlighting in it. */
var raw = code.replace(/<.*?>/g, '').replace(/&.*?;/g, ' ');
* of the line length without all the syntax highlighting in it.
* We also need to expand tab characters to the long end of
* likely values to make sure long tab-indented lines are
* processed. */
var raw = code.replace(/<.*?>/g, '').replace(/&.*?;/g, ' ').replace(/\t/g, ' ');
if (raw.length > 80) {
classes.push('longline');
}
@ -123,13 +126,11 @@ jQuery.fn.numberInput = function(increment) {
var height = $self.outerHeight() / 2;
var $dom = $('<span>').attr({ 'class': 'number-combo' })
.append($('<a>').attr({ 'class': 'number-combo-button-down',
'href': '#' })
.text('↓'))
.append($('<a>').attr({ 'class': 'number-combo-button-up',
'href': '#' })
.text('↑'));
var $dom = $('<span>', { 'class': 'number-combo' })
.append($('<a>', { 'class': 'number-combo-button-down',
'href': '#', 'text': '↓' }))
.append($('<a>', { 'class': 'number-combo-button-up',
'href': '#', 'text': '↑' }));
var $up = $dom.find('.number-combo-button-up').click(_pd(function(event, count) {
count = count || (event.ctrlKey ? increment : 1) || 1;
@ -165,6 +166,22 @@ jQuery.fn.numberInput = function(increment) {
return this;
};
jQuery.fn.appendMessage = function(message) {
$(this).each(function() {
var $self = $(this),
$container = $self.find('.message-inner');
if (!$container.length) {
$container = $('<div>', { 'class': 'message-inner' });
$self.append($('<div>', { 'class': 'message' }).append($container))
.addClass('message-container');
}
$container.append($('<div>')[typeof message == "string" ? 'text' : 'html'](message));
});
return this;
};
function bind_viewer(nodes) {
$.each(nodes, function(x) {
nodes['$'+x] = $(nodes[x]);
@ -197,16 +214,19 @@ function bind_viewer(nodes) {
/* We need to re-size the line numbers correctly depending upon
the wrapping. */
var self = this;
$node.each(function(){
$node.each(function() {
var $self = $(this),
long_lines = $(this).find('td.code div.longline');
long_lines = $(this).find('td.code div.longline'),
changes = [];
/* Use the longline hint to guess at long lines and
* see what needs resizing. */
$.each(long_lines, function() {
var $this = $(this),
k = parseInt($this.attr('class').match(/index(\d+)/)[1], 10);
$self.find('td.gutter div.index' + k).css('height', $this.height() + 'px');
changes.push(['td.gutter div.index' + k + ':eq(0)', $this.height() + 'px']);
});
$.each(changes, function(i, change) {
$self.find(change[0]).css('height', change[1]);
});
});
this.updateViewport(true);
@ -235,13 +255,64 @@ function bind_viewer(nodes) {
// Note SyntaxHighlighter has nuked the node and replaced it.
$diff = node.find('#diff');
this.size_line_numbers($diff, true);
}
this.compute_messages(node);
if (window.location.hash && window.location.hash != 'top') {
window.location = window.location;
}
};
this.message_type_map = {
'error': 'error',
'warning': 'warning',
'notice': 'info'
};
this.compute_messages = function(node) {
var $diff = node.find('#diff'),
path = this.nodes.$files.find('a.file.selected').attr('data-short');
if (this.messages && this.messages.hasOwnProperty(path)) {
var messages = this.messages[path];
for (var i = 0; i < messages.length; i++) {
var message = messages[i],
$line = $('#L' + message.line),
title = $line.attr('title'),
$dom = $('<div>').append($('<strong>').html(format('{0}{1}: {2}',
message.type[0].toUpperCase(),
message.type.substr(1),
message.message)));
$.each([].concat(message.description), function(i, msg) {
$dom.append($('<p>').html(String(message.description)));
});
if (message.line != null && $line.length) {
$line.addClass(message.type)
.parent()
.appendMessage($dom);
$('.code .' + $line.parent().attr('class').match(/number\d+/)[0] + ':eq(0)')
.addClass(message.type);
} else {
$('#diff-wrapper').before(
$('<div>', { 'class': 'notification-box' })
.addClass(this.message_type_map[message.type])
.append($dom));
}
}
}
if ($diff.length || messages) {
/* Build out the diff bar based on the line numbers. */
var $sb = $diff.siblings('.diff-bar').eq(0),
$gutter = $diff.find('td.gutter'),
$lines = $gutter.find('div.line a');
var $sb = $('.diff-bar'),
$gutter = $('td.gutter'),
$lines = $gutter.find('div.line > a');
$sb.empty();
if ($lines.length) {
var changes = [];
var flush = function($line, bottom) {
var height = (bottom - $start.offset().top) * 100 / $gutter.height(),
style = { 'height': height + "%" };
@ -255,15 +326,14 @@ function bind_viewer(nodes) {
style['margin-bottom'] = '-1px';
}
$sb.append($('<a>', { 'href': $start.attr('href'), 'class': $start.attr('class'),
'css': style }));
changes.push([$start, style]);
$prev = $start;
$start = $line;
};
var $prev, $start = null;
$lines.each(function () {
$lines.each(function() {
var $line = $(this);
if (!$start) {
$start = $line;
@ -273,6 +343,17 @@ function bind_viewer(nodes) {
});
flush(null, $gutter.offset().top + $gutter.height());
$.each(changes, function(i, change) {
var $start = change[0], style = change[1];
var $link = $('<a>', { 'href': $start.attr('href'), 'class': $start.attr('class'),
'css': style }).appendTo($sb);
if ($start.is('.error, .notice, .warning')) {
$link.appendMessage($start.parent().find('.message-inner > div').clone());
}
});
this.$diffbar = $sb;
this.$viewport = $('<div>', { 'class': 'diff-bar-viewport' });
this.$gutter = $gutter;
@ -284,9 +365,85 @@ function bind_viewer(nodes) {
this.updateViewport(true);
}
}
}
this.update_validation = function(data) {
var viewer = this;
if (window.location.hash && window.location.hash != 'top') {
window.location = window.location;
$('#validating').hide();
if (data.validation) {
this.validation = data.validation;
this.messages = {};
var messages = data.validation.messages;
for (var i = 0; i < messages.length; i++) {
var path = [].concat(messages[i].file).join("/");
if (!this.messages[path]) {
this.messages[path] = [];
}
this.messages[path].push(messages[i]);
}
this.known_files = {};
var metadata = data.validation.metadata;
if (metadata && metadata.jetpack_identified_files) {
var files = metadata.jetpack_identified_files;
for (var file in files) {
this.known_files[file] = ['JetPack'].concat(files[file]);
}
}
this.nodes.$files.find('.file').each(function() {
var $self = $(this);
var known = viewer.known_files[$self.attr('data-short')];
if (known) {
$self.attr('title',
format('Identified:\n' +
' Library: {0} {2}\n' +
' Original path: {1}',
known))
.addClass('known')
.addClass('tooltip');
}
var messages = viewer.messages[$self.attr('data-short')];
if (messages) {
var types = {};
for (var i = 0; i < messages.length; i++) {
types[messages[i].type] = true;
}
for (var type in types) {
$self.addClass(type);
}
}
});
this.nodes.$files.find('.directory').each(function() {
var $self = $(this);
var $ul = $self.parent().next();
$.each(['warning', 'error', 'notice'], function(i, type) {
if ($ul.find('.' + type + ':eq(0)').length) {
$self.addClass(type);
}
});
if (!$ul.find('.file:not(.known):eq(0)').length) {
$self.addClass('known');
}
});
this.compute_messages($('#content-wrapper'));
}
var error = data.error || typeof data == "string" && data ||
data && typeof data != "object";
if (error) {
$('#validating').after(
$('<div>', { 'class': 'notification-box error',
'text': format('{1} {2}',
$("#metadata").attr('data-validation-failed'),
error) }));
}
};
this.updateViewport = function(resize) {
@ -301,18 +458,15 @@ function bind_viewer(nodes) {
var gutter = $gutter[0].getBoundingClientRect(),
gutter_height = gutter.bottom - gutter.top,
window_height = $(window).height(),
diffbar_top = -Math.min(0, gutter.top) * 100 / gutter_height + "%";
diffbar_top = -Math.min(0, gutter.top) * 100 / gutter_height + "%",
height = Math.max(0, gutter_height + Math.min(0, gutter.top) - Math.max(gutter.bottom - window_height, 0));
if (resize) {
var height = gutter_height + Math.min(0, gutter.top) - Math.max(gutter.bottom - window_height, 0);
$viewport.css({ 'height': height * 100 / gutter_height + "%", 'top': diffbar_top });
$diffbar.css({ 'height': Math.min($("#diff-wrapper").height(), window_height) + "px" });
} else {
$viewport.css({ 'top': diffbar_top });
$diffbar.css({ 'height': Math.min($('#diff-wrapper').height(), window_height) + "px" });
}
$viewport.css({ 'height': height * 100 / gutter_height + '%', 'top': diffbar_top });
if (gutter.bottom <= window_height) {
$diffbar.css({ 'position': 'absolute', 'top': '', 'bottom': '0' });
} else if (gutter.top > 0) {
@ -398,11 +552,13 @@ function bind_viewer(nodes) {
});
return k;
};
this.toggle_wrap = function(state) {
this.toggle_wrap = function(state, quick) {
/* Toggles the content wrap in the page, starts off wrapped */
this.wrapped = (state == 'wrap' || !this.wrapped);
$('code').toggleClass('unwrapped');
this.size_line_numbers($('#content-wrapper'), false);
if (!quick) {
this.size_line_numbers($('#content-wrapper'), false);
}
};
this.toggle_files = function(state) {
this.hidden = (state == 'hide' || !this.hidden);
@ -421,13 +577,14 @@ function bind_viewer(nodes) {
this.next_changed = function(offset) {
var $files = this.nodes.$files.find('a.file'),
selected = $files[this.get_selected()],
isDiff = $('#diff').length;
isDiff = $('#diff').length,
filter = (isDiff ? '.diff' : '') + ':not(.known)';
var a = [], list = a;
$files.each(function () {
$files.each(function() {
if (this == selected) {
list = [selected];
} else if (config.needreview_pattern.test($(this).attr('data-short')) && (!isDiff || $(this).hasClass('diff'))) {
} else if (config.needreview_pattern.test($(this).attr('data-short')) && $(this).is(filter)) {
list.push(this);
}
});
@ -439,7 +596,9 @@ function bind_viewer(nodes) {
}
};
this.next_delta = function(forward) {
var $deltas = $('td.code .line.add, td.code .line.delete'),
var classes = $("#diff").length ? 'add delete' : 'warning notice error',
$deltas = $(classes.split(/ /g)
.map(function(className) { return 'td.code .line.' + className; }).join(', ')),
$lines = $('td.code .line');
$lines.indexOf = Array.prototype.indexOf;
@ -488,10 +647,8 @@ function bind_viewer(nodes) {
viewer.toggle_leaf($(this));
}));
if ($('#diff').length) {
$(window).resize(debounce(function () { viewer.updateViewport(true); }));
$(window).scroll(debounce(function () { viewer.updateViewport(false); }));
}
$(window).resize(debounce(function() { viewer.updateViewport(true); }));
$(window).scroll(debounce(function() { viewer.updateViewport(false); }));
$('#files-up').click(_pd(function() {
viewer.next_changed(-1);
@ -523,16 +680,34 @@ function bind_viewer(nodes) {
});
}));
if ($('#metadata').attr('data-validate-url')) {
$('#validating').css('display', 'block');
$.ajax({type: 'POST',
url: $('#metadata').attr('data-validate-url'),
data: {},
success: function(data) {
viewer.update_validation(data);
},
error: function(XMLHttpRequest, textStatus, errorThrown) {
viewer.update_validation(textStatus);
},
dataType: 'json'
});
}
viewer.nodes.$files.find('.file').click(_pd(function() {
viewer.select($(this));
viewer.toggle_wrap('wrap');
viewer.toggle_wrap('wrap', true);
}));
$(window).bind('popstate', function() {
if (viewer.last != location.pathname) {
viewer.nodes.$files.find('.file').each(function() {
if ($(this).attr('href') == location.pathname) {
viewer.select($(this));
if (!$(this).is('.selected')) {
viewer.select($(this));
}
}
});
}
@ -540,7 +715,7 @@ function bind_viewer(nodes) {
var prefixes = {},
keys = {};
$('#commands code').each(function () {
$('#commands code').each(function() {
var $code = $(this),
$link = $code.parents('tr').find('a'),
key = $code.text();
@ -553,7 +728,9 @@ function bind_viewer(nodes) {
var buffer = '';
$(document).bind('keypress', function(e) {
if (e.charCode && !(e.altKey || e.ctrlKey || e.metaKey)) {
if (e.charCode && !(e.altKey || e.ctrlKey || e.metaKey) &&
![HTMLInputElement, HTMLSelectElement, HTMLTextAreaElement]
.some(function (iface) { return e.target instanceof iface })) {
buffer += String.fromCharCode(e.charCode);
if (keys.hasOwnProperty(buffer)) {
e.preventDefault();
@ -581,19 +758,20 @@ function bind_viewer(nodes) {
var $tabstops = $('#tab-stops')
.numberInput(4)
.val(Number(storage.get(localTabstopsKey) || storage.get(tabstopsKey)) || 4)
.change(function() {
.change(function(event, global) {
rule.style.tabSize = rule.style.MozTabSize = $(this).val();
storage.set(localTabstopsKey, $(this).val());
if (!global)
storage.set(localTabstopsKey, $(this).val());
storage.set(tabstopsKey, $(this).val());
})
.change();
.trigger('change', true);
}
return viewer;
}
var viewer = null;
$(document).ready(function() {
var viewer = null;
var nodes = { files: '#files', thinking: '#thinking', commands: '#commands' };
function poll_file_extraction() {
$.getJSON($('#extracting').attr('data-url'), function(json) {

Просмотреть файл

@ -44,21 +44,21 @@ jQuery.fn.tooltip = function(tip_el) {
timeout = false,
$tgt, $title, delay;
function setTip() {
if (!$tgt) return;
var pos = $tgt.offset(),
title = $title.attr('title');
title = $title.attr('title'),
html = $title.attr('data-tooltip-html');
delay = $title.is('[data-delay]') ? $title.attr('data-delay') : 300;
if(title.indexOf('::') > 0) {
if(!html && title.indexOf('::') > 0) {
var title_split = title.split('::');
$msg.text("");
$msg.append($("<strong>", {'text': title_split[0].trim()}));
$msg.append($("<span>", {'text': title_split[1].trim()}));
} else {
$msg.text(title);
$msg[html ? 'html' : 'text'](title);
}
$title.attr('data-oldtitle', title).attr('title', '');