diff --git a/dreditor.user.js b/dreditor.user.js index 676800c..0a57bc5 100644 --- a/dreditor.user.js +++ b/dreditor.user.js @@ -67,7 +67,6 @@ if (typeof __PAGE_SCOPE_RUN__ == 'undefined') { // End execution. This code path is only reached in a GreaseMonkey/user // script environment. - return; } // @todo Implement closure to provide jQuery in $. @@ -77,10 +76,11 @@ if (typeof __PAGE_SCOPE_RUN__ == 'undefined') { // the scripts on the actual page are executed. Cancel processing in this case. // Drupal is also undefined when drupal.org is down. // @todo Verify whether this still applies. -if (typeof Drupal == 'undefined') { - return; +else if (typeof Drupal == 'undefined') { + // } +else { /** * @defgroup jquery_extensions jQuery extensions * @{ @@ -616,6 +616,56 @@ Drupal.storage.unserialize = function (str) { }; /** + * Mimic of d.o. cache_get cache_set and cache_clear + * + * Not we only have only a key/value store so there is no cache table + * + * There are cache keys containing KEYs to manage. The actual cache item is + * stored under it's own KEY. + */ +Drupal.storage.cache = { + getCache : function(cache) { + return cache ? cache : 'cache'; + }, + + getKeys : function(cache) { + cache = Drupal.storage.cache.getCache(cache); + var keys = Drupal.storage.load(cache); + return keys ? keys.split(';') : []; + }, + + set : function(id, data, cache) { + cache = Drupal.storage.cache.getCache(cache); + var keys = Drupal.storage.cache.getKeys(cache); + + if (keys.indexOf(id) == -1) { + keys.push(id); + } + // Save both cachekeys and cachable data @see Drupal.storage.cache + Drupal.storage.save(id, data); + Drupal.storage.save(cache, keys.join(';')); + }, + + get : function(id, cache) { + var keys = Drupal.storage.cache.getKeys(cache); + if (keys.indexOf(id) > -1) { + return Drupal.storage.load(id); + } + }, + + clear : function(cache) { + cache = Drupal.storage.cache.getCache(cache); + var keys = Drupal.storage.cache.getKeys(cache); + // Delete both cachable data as keys @see Drupal.storage.cache + $.each(keys, function(i, value) { + Drupal.storage.remove(value); + }); + Drupal.storage.remove(cache); + } +} + + +/** * @defgroup form_api JavaScript port of Drupal Form API * @{ */ @@ -664,6 +714,332 @@ Drupal.dreditor.form.form.prototype = { */ /** + * @defgroup macro + * @{ + */ + +/** + * Dreditor Macro support. + * + * A macro is just a simple construct @(set-value) + * - @status(3) + * - @comment(some text appended) + * - @tags(ux) + * + * To check for field available on a drupal form page this helps a little. + * Run it from within firebug. + * + var list=[]; + $('#comment-form').find('*[@id]').each(function() { + list.push($(this).attr('id')); + }); + list.filter(function(elem){ + return !elem.match(/wrapper/); + }); + list.join(" "); + */ +Drupal.dreditor.macro = { + values : {}, + patterns : { + query : /^@(.*)\(\?\)$/m, + + // Make sure to accept multiple lines + get : /@([a-z_]*)?\(\)/m, + + oldGet : /@old([A-Z][a-z_]*)?\(([^\(\)]*)\)/m, + + // May contains a get: @comment(This is duplicate of @duplicate_issue()) + set: /^@([a-z_]+)\(((?:[^\(\)]+\([^\(\)]*\)+|[^\(\)])*)\)/m + }, + /** + * + * Provide get/set on form field + * + * We support the following types + * - text : get/set uses the whole field + * - textarea : set appends values + * - tags : set handles comma + */ + fields : { + title : { + id : "edit-title", + type : 'text' + }, + project_title : { + label : 'Project:', + id : "edit-project-info-project-title", + type : 'text', + readonly : true + }, + version : { + label : 'Version:', + id : "edit-project-info-rid", + type : 'select', + readonly : true + }, + assigned : { + label : 'Assigned:', + id : "edit-project-info-assigned", + type : 'select' + }, + component : { + label : 'Component:', + id : 'edit-project-info-component', + type : 'select' + }, + category : { + label : 'Category:', + id : "edit-category", + type : 'select' + }, + priority : { + label : 'Priority:', + id : "edit-priority", + type : 'select', + label : 'Priority:' + }, + status : { + label : 'Status:', + id : "edit-sid", + type : 'select', + label : 'Status:' + }, + comment : { + id : "edit-comment", + type : 'textarea' + }, + tags : { + label : 'Tags:', + id : "edit-taxonomy-tags-9", + type : 'tags' + } + }, + + set : function (name, value) { + function select_set($f, value) { + // First try machine values + var $option_value = $f.find('option[@value='+ value +']'); + if ($option_value.length > 0) { + $f.val($option_value.val()); + return; + } + // Try human readable values + var $option_text = $f.find('option') + .filter(function () { + return this.text() == value + } + ); + if ($option_text.length > 0){ + $f.val($option_text.attr('value')); + } + } + + function tags_set($f, value) { + var remove = false; + if (value.indexOf("-") == 0) { + remove = true; + value = value.replace(/^\-/, ''); + } + var val = $f.val(); + var values = []; + if (val.length > 0) { + values = val.split(/\W*,\W*/); + } + var position = $.inArray(value, values) + if ( position > -1) { + if (remove) { + values.splice(position, 1); + } + } + else { + values.push(value); + } + $f.val(values.join(',')); + } + + /** + * A textarea gets new values appended + * + * If not empty we prepend new-lines first + */ + function textarea_set($f, value) { + var val = $f.val(); + if (val.length > 0) { + $f.val(val + "\n\n" + value); + return; + } + $f.val(value); + } + + var f = Drupal.dreditor.macro.fields[name]; + if (typeof f != 'undefined') { + if (typeof f.readonly != 'undefined' && f.readonly) { + return; + } + $f = jQuery('#' + f.id); + // bail out if not defined + if ($f.length == 0) return; + + if (f.type == 'textarea') { + textarea_set($f, value) + } + else if (f.type == 'tags') { + tags_set($f, value) + } + else { + $f.val(value); + } + $f.animate( {opacity: 'toggle'}).animate( {opacity: 'toggle'}); + } + }, + + /** + * The current issue values are presented at the top of an issue + * + * The values are themed into a table id=project-issue-summary-table + * + * Each row has two column + * - first having the label listed above in the fields + * - next having the current issue status value + */ + getCurrent : function (oldName) { + var name = oldName.replace(/^old/, '').toLowerCase(); + var f = Drupal.dreditor.macro.fields[name]; + if (typeof f != 'undefined') { + if (typeof f.label != 'undefined') { + var label = f.label; + var value; + $rows = $('#project-issue-summary-table').find('tr'); + $.each($rows, function(index, row) { + var name = $(row.cells[0]).text(); + var val = $(row.cells[1]).text(); + if (name == label) { + value = val; + } + }); + return value; + } + } + }, + + /** + * Get the value of the named field + * + * @name + * The registered named field + * @readable + * The textual variant for select + * + * @return the machina value or the readable variant + */ + get : function (name, readable) { + function select_get($f, readable) { + if (readable) { + return $f.find('option[@value='+ $f.val()+']').text(); + } + else { + return $f.val(); + } + } + + if (typeof readable == 'undefined') { + readable = true; + } + var f = Drupal.dreditor.macro.fields[name]; + if (typeof f != 'undefined') { + var $f = jQuery("#" + f.id); + // bail out if not defined + if ($f.length == 0) return; + + if (f.type == 'select') { + return select_get($f, readable); + } + return $f.val(); + } + else { + // Try values + return Drupal.dreditor.macro.values[name]; + } + }, + + /** + * Parses a string optional containing macro's + * + * Examples + * - needs work @status(13)@tags(documentation) + */ + parse : function (text) { + var pattern = Drupal.dreditor.macro.patterns.get; + var matches = text.match(pattern); + while (matches) { + var name = matches[1]; + text = text.replace(pattern, Drupal.dreditor.macro.get(name)); + matches = text.match(pattern); + } + return text; + }, + + /** + * An array of commands are executed on by one + * + * Each item may contains more then one macro @id(value) + * + * @commands array + * Contains strings with macro's + */ + execute : function (commands) { + jQuery.each(commands, function(key, value) { + console.log('Executing: ' + key + ":'" + value + "'"); + var pattern = Drupal.dreditor.macro.patterns.set + var matches = value.match(pattern); + while (matches) { + var cmd = matches[1].replace(/^\s*/, '').replace(/\s*$/, ''); + // trim value + var val = matches[2].replace(/^\s*/m, '').replace(/\s*$/m, ''); + // consume value + value = value.replace(pattern, ''); + + if (val == '?') { + // Implement a dialog + var msg = ''; + var def = ''; + var pattern = ''; + if (cmd == 'duplicate_issue' || cmd =='depends_on_issue' || cmd == 'blocks_issue') { + msg = "Please give issue #"; + pattern = '[#@]'; + def = '1125936'; + } + else if (cmd == 'duplicate_project') { + msg = "Please give project name"; + pattern = 'http://drupal.org/project/@'; + def = 'dreditor'; + } + + if (msg.length) { + var result = window.prompt(msg, def); + + result = pattern.replace('@', result); + // Store values for later retrieval + Drupal.dreditor.macro.values[cmd] = result; + } + } + else { + // Process getters like @duplicate_issue() first + val = Drupal.dreditor.macro.parse(val); + Drupal.dreditor.macro.set(matches[1], val); + } + matches = value.match( Drupal.dreditor.macro.patterns.set); + } + }); + } +}; + +/** + * @} End of "defgroup macro". + */ + + +/** * Attach patch review editor to issue attachments. */ Drupal.behaviors.dreditorPatchReview = function (context) { @@ -1604,9 +1980,416 @@ Drupal.behaviors.dreditorCommitMessage = function (context) { $(this).prepend($container); } $link.prependTo($container); + + // Inject triage + Drupal.dreditor.triage.setup($container); }); }; +Drupal.dreditor.triage = { + injectCSS : function() { + $('head').append( + ''); + }, + + getHideIrrelevant : function() { + var current = Drupal.storage.load('triage-hide-irrelevant'); + if (typeof current == 'undefined') { + current = false; + Drupal.storage.save('triage-hide-irrelevant', current); + } + return current; + }, + + setHideIrrelevant : function(current) { + Drupal.storage.save('triage-hide-irrelevant', current); + return current + }, + + hideIrrelevant : function(toggle) { + if (typeof toggle == 'undefined') { + toggle = true; + } + var current = Drupal.dreditor.triage.getHideIrrelevant(); + if (toggle) { + current = !current; + } + + if (current) { + $('.triage-top-level').addClass('triage-hide-irrelevant'); + } + else { + $('.triage-top-level').removeClass('triage-hide-irrelevant'); + } + + Drupal.dreditor.triage.setHideIrrelevant(current); + return current; + }, + /** + * The root book page containing triage child pages + * + * For now we only use the child pages of the root + * + * We could use the hierarchy later on to add project + * specific sub Macro and Templates + */ + getTriageRoot : function() { + return { + id : 'node-1120672', + url : "http://drupal.org/node/1120672", + description : 'The root for all triage nodes' + }; + }, + + /** + * Downloads a file and then store it for later reuse + * + * As we can only get data from the same domain + * we better log bad requests to help our users. + * + * So in our tests we populate the cache so bypass this. + * + * @see http://en.wikipedia.org/wiki/Same_origin_policy + */ + getFile : function(id, src, callback) { + var data = Drupal.storage.cache.get(id, 'triage'); + if (data) { + console.log("getFile: cache hit: " + id); + callback(data); + } + else { + console.log("getFile: cache miss: " + id); + $.ajax({ + url : src, + success: function(data) { + Drupal.storage.cache.set(id, data, 'triage'); + callback(data); + }, + dataType: 'html' + }); + } + + }, + + /** + * The menu for triage + */ + menu : function() { + var $triageOptions = $('#dreditor-triage-options'); + $triageOptions.css('float', 'right'); + + var $ul = $('