Commit 63c2c2cc23eeacfda0f15c339dbaf854e7de1199

Authored by Dmitriy Zaporozhets
2 parents 8a8fa8b0 e29a2566

Merge branch 'dev'

Showing 59 changed files with 2086 additions and 350 deletions   Show diff stats
Gemfile
... ... @@ -21,6 +21,8 @@ gem "git"
21 21 gem "acts_as_list"
22 22 gem 'rdiscount'
23 23  
  24 +gem 'acts-as-taggable-on', '~>2.1.0'
  25 +
24 26 group :assets do
25 27 gem 'sass-rails', " ~> 3.1.0"
26 28 gem 'coffee-rails', "~> 3.1.0"
... ...
Gemfile.lock
... ... @@ -54,6 +54,8 @@ GEM
54 54 activesupport (= 3.1.0)
55 55 activesupport (3.1.0)
56 56 multi_json (~> 1.0)
  57 + acts-as-taggable-on (2.1.1)
  58 + rails
57 59 acts_as_list (0.1.4)
58 60 addressable (2.2.6)
59 61 ansi (1.3.0)
... ... @@ -246,6 +248,7 @@ PLATFORMS
246 248 ruby
247 249  
248 250 DEPENDENCIES
  251 + acts-as-taggable-on (~> 2.1.0)
249 252 acts_as_list
250 253 annotate!
251 254 autotest
... ...
app/assets/images/ajax-loader.gif

6.66 KB | W: | H:

4.08 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/chosen-sprite.png 0 → 100644

396 Bytes

app/assets/javascripts/application.js
... ... @@ -8,6 +8,7 @@
8 8 //= require jquery-ui
9 9 //= require jquery_ujs
10 10 //= require jquery.ui.selectmenu
  11 +//= require jquery.tagify
11 12 //= require jquery.cookie
12 13 //= require_tree .
13 14  
... ... @@ -20,6 +21,6 @@ $(function(){
20 21 $('select#tag').selectmenu({style:'popup', width:200});
21 22 });
22 23  
23   -function updatePage(){
24   - $.ajax({type: "GET", url: location.href, dataType: "script"});
  24 +function updatePage(data){
  25 + $.ajax({type: "GET", url: location.href, data: data, dataType: "script"});
25 26 }
... ...
app/assets/javascripts/chosen.jquery.js 0 → 100644
... ... @@ -0,0 +1,901 @@
  1 +// Chosen, a Select Box Enhancer for jQuery and Protoype
  2 +// by Patrick Filler for Harvest, http://getharvest.com
  3 +//
  4 +// Version 0.9.5
  5 +// Full source at https://github.com/harvesthq/chosen
  6 +// Copyright (c) 2011 Harvest http://getharvest.com
  7 +
  8 +// MIT License, https://github.com/harvesthq/chosen/blob/master/LICENSE.md
  9 +// This file is generated by `cake build`, do not edit it by hand.
  10 +(function() {
  11 + var SelectParser;
  12 + SelectParser = (function() {
  13 + function SelectParser() {
  14 + this.options_index = 0;
  15 + this.parsed = [];
  16 + }
  17 + SelectParser.prototype.add_node = function(child) {
  18 + if (child.nodeName === "OPTGROUP") {
  19 + return this.add_group(child);
  20 + } else {
  21 + return this.add_option(child);
  22 + }
  23 + };
  24 + SelectParser.prototype.add_group = function(group) {
  25 + var group_position, option, _i, _len, _ref, _results;
  26 + group_position = this.parsed.length;
  27 + this.parsed.push({
  28 + array_index: group_position,
  29 + group: true,
  30 + label: group.label,
  31 + children: 0,
  32 + disabled: group.disabled
  33 + });
  34 + _ref = group.childNodes;
  35 + _results = [];
  36 + for (_i = 0, _len = _ref.length; _i < _len; _i++) {
  37 + option = _ref[_i];
  38 + _results.push(this.add_option(option, group_position, group.disabled));
  39 + }
  40 + return _results;
  41 + };
  42 + SelectParser.prototype.add_option = function(option, group_position, group_disabled) {
  43 + if (option.nodeName === "OPTION") {
  44 + if (option.text !== "") {
  45 + if (group_position != null) {
  46 + this.parsed[group_position].children += 1;
  47 + }
  48 + this.parsed.push({
  49 + array_index: this.parsed.length,
  50 + options_index: this.options_index,
  51 + value: option.value,
  52 + text: option.text,
  53 + html: option.innerHTML,
  54 + selected: option.selected,
  55 + disabled: group_disabled === true ? group_disabled : option.disabled,
  56 + group_array_index: group_position,
  57 + classes: option.className,
  58 + style: option.style.cssText
  59 + });
  60 + } else {
  61 + this.parsed.push({
  62 + array_index: this.parsed.length,
  63 + options_index: this.options_index,
  64 + empty: true
  65 + });
  66 + }
  67 + return this.options_index += 1;
  68 + }
  69 + };
  70 + return SelectParser;
  71 + })();
  72 + SelectParser.select_to_array = function(select) {
  73 + var child, parser, _i, _len, _ref;
  74 + parser = new SelectParser();
  75 + _ref = select.childNodes;
  76 + for (_i = 0, _len = _ref.length; _i < _len; _i++) {
  77 + child = _ref[_i];
  78 + parser.add_node(child);
  79 + }
  80 + return parser.parsed;
  81 + };
  82 + this.SelectParser = SelectParser;
  83 +}).call(this);
  84 +(function() {
  85 + /*
  86 + Chosen source: generate output using 'cake build'
  87 + Copyright (c) 2011 by Harvest
  88 + */
  89 + var AbstractChosen, root;
  90 + var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
  91 + root = this;
  92 + AbstractChosen = (function() {
  93 + function AbstractChosen(form_field, options) {
  94 + this.form_field = form_field;
  95 + this.options = options != null ? options : {};
  96 + this.set_default_values();
  97 + this.is_multiple = this.form_field.multiple;
  98 + this.default_text_default = this.is_multiple ? "Select Some Options" : "Select an Option";
  99 + this.setup();
  100 + this.set_up_html();
  101 + this.register_observers();
  102 + this.finish_setup();
  103 + }
  104 + AbstractChosen.prototype.set_default_values = function() {
  105 + this.click_test_action = __bind(function(evt) {
  106 + return this.test_active_click(evt);
  107 + }, this);
  108 + this.activate_action = __bind(function(evt) {
  109 + return this.activate_field(evt);
  110 + }, this);
  111 + this.active_field = false;
  112 + this.mouse_on_container = false;
  113 + this.results_showing = false;
  114 + this.result_highlighted = null;
  115 + this.result_single_selected = null;
  116 + this.allow_single_deselect = (this.options.allow_single_deselect != null) && this.form_field.options[0].text === "" ? this.options.allow_single_deselect : false;
  117 + this.disable_search_threshold = this.options.disable_search_threshold || 0;
  118 + this.choices = 0;
  119 + return this.results_none_found = this.options.no_results_text || "No results match";
  120 + };
  121 + AbstractChosen.prototype.mouse_enter = function() {
  122 + return this.mouse_on_container = true;
  123 + };
  124 + AbstractChosen.prototype.mouse_leave = function() {
  125 + return this.mouse_on_container = false;
  126 + };
  127 + AbstractChosen.prototype.input_focus = function(evt) {
  128 + if (!this.active_field) {
  129 + return setTimeout((__bind(function() {
  130 + return this.container_mousedown();
  131 + }, this)), 50);
  132 + }
  133 + };
  134 + AbstractChosen.prototype.input_blur = function(evt) {
  135 + if (!this.mouse_on_container) {
  136 + this.active_field = false;
  137 + return setTimeout((__bind(function() {
  138 + return this.blur_test();
  139 + }, this)), 100);
  140 + }
  141 + };
  142 + AbstractChosen.prototype.result_add_option = function(option) {
  143 + var classes, style;
  144 + if (!option.disabled) {
  145 + option.dom_id = this.container_id + "_o_" + option.array_index;
  146 + classes = option.selected && this.is_multiple ? [] : ["active-result"];
  147 + if (option.selected) {
  148 + classes.push("result-selected");
  149 + }
  150 + if (option.group_array_index != null) {
  151 + classes.push("group-option");
  152 + }
  153 + if (option.classes !== "") {
  154 + classes.push(option.classes);
  155 + }
  156 + style = option.style.cssText !== "" ? " style=\"" + option.style + "\"" : "";
  157 + return '<li id="' + option.dom_id + '" class="' + classes.join(' ') + '"' + style + '>' + option.html + '</li>';
  158 + } else {
  159 + return "";
  160 + }
  161 + };
  162 + AbstractChosen.prototype.results_update_field = function() {
  163 + this.result_clear_highlight();
  164 + this.result_single_selected = null;
  165 + return this.results_build();
  166 + };
  167 + AbstractChosen.prototype.results_toggle = function() {
  168 + if (this.results_showing) {
  169 + return this.results_hide();
  170 + } else {
  171 + return this.results_show();
  172 + }
  173 + };
  174 + AbstractChosen.prototype.results_search = function(evt) {
  175 + if (this.results_showing) {
  176 + return this.winnow_results();
  177 + } else {
  178 + return this.results_show();
  179 + }
  180 + };
  181 + AbstractChosen.prototype.keyup_checker = function(evt) {
  182 + var stroke, _ref;
  183 + stroke = (_ref = evt.which) != null ? _ref : evt.keyCode;
  184 + this.search_field_scale();
  185 + switch (stroke) {
  186 + case 8:
  187 + if (this.is_multiple && this.backstroke_length < 1 && this.choices > 0) {
  188 + return this.keydown_backstroke();
  189 + } else if (!this.pending_backstroke) {
  190 + this.result_clear_highlight();
  191 + return this.results_search();
  192 + }
  193 + break;
  194 + case 13:
  195 + evt.preventDefault();
  196 + if (this.results_showing) {
  197 + return this.result_select(evt);
  198 + }
  199 + break;
  200 + case 27:
  201 + if (this.results_showing) {
  202 + return this.results_hide();
  203 + }
  204 + break;
  205 + case 9:
  206 + case 38:
  207 + case 40:
  208 + case 16:
  209 + case 91:
  210 + case 17:
  211 + break;
  212 + default:
  213 + return this.results_search();
  214 + }
  215 + };
  216 + AbstractChosen.prototype.generate_field_id = function() {
  217 + var new_id;
  218 + new_id = this.generate_random_id();
  219 + this.form_field.id = new_id;
  220 + return new_id;
  221 + };
  222 + AbstractChosen.prototype.generate_random_char = function() {
  223 + var chars, newchar, rand;
  224 + chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZ";
  225 + rand = Math.floor(Math.random() * chars.length);
  226 + return newchar = chars.substring(rand, rand + 1);
  227 + };
  228 + return AbstractChosen;
  229 + })();
  230 + root.AbstractChosen = AbstractChosen;
  231 +}).call(this);
  232 +(function() {
  233 + /*
  234 + Chosen source: generate output using 'cake build'
  235 + Copyright (c) 2011 by Harvest
  236 + */
  237 + var $, Chosen, get_side_border_padding, root;
  238 + var __hasProp = Object.prototype.hasOwnProperty, __extends = function(child, parent) {
  239 + for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; }
  240 + function ctor() { this.constructor = child; }
  241 + ctor.prototype = parent.prototype;
  242 + child.prototype = new ctor;
  243 + child.__super__ = parent.prototype;
  244 + return child;
  245 + }, __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
  246 + root = this;
  247 + $ = jQuery;
  248 + $.fn.extend({
  249 + chosen: function(options) {
  250 + if ($.browser.msie && ($.browser.version === "6.0" || $.browser.version === "7.0")) {
  251 + return this;
  252 + }
  253 + return $(this).each(function(input_field) {
  254 + if (!($(this)).hasClass("chzn-done")) {
  255 + return new Chosen(this, options);
  256 + }
  257 + });
  258 + }
  259 + });
  260 + Chosen = (function() {
  261 + __extends(Chosen, AbstractChosen);
  262 + function Chosen() {
  263 + Chosen.__super__.constructor.apply(this, arguments);
  264 + }
  265 + Chosen.prototype.setup = function() {
  266 + this.form_field_jq = $(this.form_field);
  267 + return this.is_rtl = this.form_field_jq.hasClass("chzn-rtl");
  268 + };
  269 + Chosen.prototype.finish_setup = function() {
  270 + return this.form_field_jq.addClass("chzn-done");
  271 + };
  272 + Chosen.prototype.set_up_html = function() {
  273 + var container_div, dd_top, dd_width, sf_width;
  274 + this.container_id = this.form_field.id.length ? this.form_field.id.replace(/(:|\.)/g, '_') : this.generate_field_id();
  275 + this.container_id += "_chzn";
  276 + this.f_width = this.form_field_jq.outerWidth();
  277 + this.default_text = this.form_field_jq.data('placeholder') ? this.form_field_jq.data('placeholder') : this.default_text_default;
  278 + container_div = $("<div />", {
  279 + id: this.container_id,
  280 + "class": "chzn-container" + (this.is_rtl ? ' chzn-rtl' : ''),
  281 + style: 'width: ' + this.f_width + 'px;'
  282 + });
  283 + if (this.is_multiple) {
  284 + container_div.html('<ul class="chzn-choices"><li class="search-field"><input type="text" value="' + this.default_text + '" class="default" autocomplete="off" style="width:25px;" /></li></ul><div class="chzn-drop" style="left:-9000px;"><ul class="chzn-results"></ul></div>');
  285 + } else {
  286 + container_div.html('<a href="javascript:void(0)" class="chzn-single"><span>' + this.default_text + '</span><div><b></b></div></a><div class="chzn-drop" style="left:-9000px;"><div class="chzn-search"><input type="text" autocomplete="off" /></div><ul class="chzn-results"></ul></div>');
  287 + }
  288 + this.form_field_jq.hide().after(container_div);
  289 + this.container = $('#' + this.container_id);
  290 + this.container.addClass("chzn-container-" + (this.is_multiple ? "multi" : "single"));
  291 + this.dropdown = this.container.find('div.chzn-drop').first();
  292 + dd_top = this.container.height();
  293 + dd_width = this.f_width - get_side_border_padding(this.dropdown);
  294 + this.dropdown.css({
  295 + "width": dd_width + "px",
  296 + "top": dd_top + "px"
  297 + });
  298 + this.search_field = this.container.find('input').first();
  299 + this.search_results = this.container.find('ul.chzn-results').first();
  300 + this.search_field_scale();
  301 + this.search_no_results = this.container.find('li.no-results').first();
  302 + if (this.is_multiple) {
  303 + this.search_choices = this.container.find('ul.chzn-choices').first();
  304 + this.search_container = this.container.find('li.search-field').first();
  305 + } else {
  306 + this.search_container = this.container.find('div.chzn-search').first();
  307 + this.selected_item = this.container.find('.chzn-single').first();
  308 + sf_width = dd_width - get_side_border_padding(this.search_container) - get_side_border_padding(this.search_field);
  309 + this.search_field.css({
  310 + "width": sf_width + "px"
  311 + });
  312 + }
  313 + this.results_build();
  314 + this.set_tab_index();
  315 + return this.form_field_jq.trigger("liszt:ready", {
  316 + chosen: this
  317 + });
  318 + };
  319 + Chosen.prototype.register_observers = function() {
  320 + this.container.mousedown(__bind(function(evt) {
  321 + return this.container_mousedown(evt);
  322 + }, this));
  323 + this.container.mouseup(__bind(function(evt) {
  324 + return this.container_mouseup(evt);
  325 + }, this));
  326 + this.container.mouseenter(__bind(function(evt) {
  327 + return this.mouse_enter(evt);
  328 + }, this));
  329 + this.container.mouseleave(__bind(function(evt) {
  330 + return this.mouse_leave(evt);
  331 + }, this));
  332 + this.search_results.mouseup(__bind(function(evt) {
  333 + return this.search_results_mouseup(evt);
  334 + }, this));
  335 + this.search_results.mouseover(__bind(function(evt) {
  336 + return this.search_results_mouseover(evt);
  337 + }, this));
  338 + this.search_results.mouseout(__bind(function(evt) {
  339 + return this.search_results_mouseout(evt);
  340 + }, this));
  341 + this.form_field_jq.bind("liszt:updated", __bind(function(evt) {
  342 + return this.results_update_field(evt);
  343 + }, this));
  344 + this.search_field.blur(__bind(function(evt) {
  345 + return this.input_blur(evt);
  346 + }, this));
  347 + this.search_field.keyup(__bind(function(evt) {
  348 + return this.keyup_checker(evt);
  349 + }, this));
  350 + this.search_field.keydown(__bind(function(evt) {
  351 + return this.keydown_checker(evt);
  352 + }, this));
  353 + if (this.is_multiple) {
  354 + this.search_choices.click(__bind(function(evt) {
  355 + return this.choices_click(evt);
  356 + }, this));
  357 + return this.search_field.focus(__bind(function(evt) {
  358 + return this.input_focus(evt);
  359 + }, this));
  360 + }
  361 + };
  362 + Chosen.prototype.search_field_disabled = function() {
  363 + this.is_disabled = this.form_field_jq[0].disabled;
  364 + if (this.is_disabled) {
  365 + this.container.addClass('chzn-disabled');
  366 + this.search_field[0].disabled = true;
  367 + if (!this.is_multiple) {
  368 + this.selected_item.unbind("focus", this.activate_action);
  369 + }
  370 + return this.close_field();
  371 + } else {
  372 + this.container.removeClass('chzn-disabled');
  373 + this.search_field[0].disabled = false;
  374 + if (!this.is_multiple) {
  375 + return this.selected_item.bind("focus", this.activate_action);
  376 + }
  377 + }
  378 + };
  379 + Chosen.prototype.container_mousedown = function(evt) {
  380 + var target_closelink;
  381 + if (!this.is_disabled) {
  382 + target_closelink = evt != null ? ($(evt.target)).hasClass("search-choice-close") : false;
  383 + if (evt && evt.type === "mousedown") {
  384 + evt.stopPropagation();
  385 + }
  386 + if (!this.pending_destroy_click && !target_closelink) {
  387 + if (!this.active_field) {
  388 + if (this.is_multiple) {
  389 + this.search_field.val("");
  390 + }
  391 + $(document).click(this.click_test_action);
  392 + this.results_show();
  393 + } else if (!this.is_multiple && evt && ($(evt.target) === this.selected_item || $(evt.target).parents("a.chzn-single").length)) {
  394 + evt.preventDefault();
  395 + this.results_toggle();
  396 + }
  397 + return this.activate_field();
  398 + } else {
  399 + return this.pending_destroy_click = false;
  400 + }
  401 + }
  402 + };
  403 + Chosen.prototype.container_mouseup = function(evt) {
  404 + if (evt.target.nodeName === "ABBR") {
  405 + return this.results_reset(evt);
  406 + }
  407 + };
  408 + Chosen.prototype.blur_test = function(evt) {
  409 + if (!this.active_field && this.container.hasClass("chzn-container-active")) {
  410 + return this.close_field();
  411 + }
  412 + };
  413 + Chosen.prototype.close_field = function() {
  414 + $(document).unbind("click", this.click_test_action);
  415 + if (!this.is_multiple) {
  416 + this.selected_item.attr("tabindex", this.search_field.attr("tabindex"));
  417 + this.search_field.attr("tabindex", -1);
  418 + }
  419 + this.active_field = false;
  420 + this.results_hide();
  421 + this.container.removeClass("chzn-container-active");
  422 + this.winnow_results_clear();
  423 + this.clear_backstroke();
  424 + this.show_search_field_default();
  425 + return this.search_field_scale();
  426 + };
  427 + Chosen.prototype.activate_field = function() {
  428 + if (!this.is_multiple && !this.active_field) {
  429 + this.search_field.attr("tabindex", this.selected_item.attr("tabindex"));
  430 + this.selected_item.attr("tabindex", -1);
  431 + }
  432 + this.container.addClass("chzn-container-active");
  433 + this.active_field = true;
  434 + this.search_field.val(this.search_field.val());
  435 + return this.search_field.focus();
  436 + };
  437 + Chosen.prototype.test_active_click = function(evt) {
  438 + if ($(evt.target).parents('#' + this.container_id).length) {
  439 + return this.active_field = true;
  440 + } else {
  441 + return this.close_field();
  442 + }
  443 + };
  444 + Chosen.prototype.results_build = function() {
  445 + var content, data, startTime, _i, _len, _ref;
  446 + startTime = new Date();
  447 + this.parsing = true;
  448 + this.results_data = root.SelectParser.select_to_array(this.form_field);
  449 + if (this.is_multiple && this.choices > 0) {
  450 + this.search_choices.find("li.search-choice").remove();
  451 + this.choices = 0;
  452 + } else if (!this.is_multiple) {
  453 + this.selected_item.find("span").text(this.default_text);
  454 + if (this.form_field.options.length <= this.disable_search_threshold) {
  455 + this.container.addClass("chzn-container-single-nosearch");
  456 + } else {
  457 + this.container.removeClass("chzn-container-single-nosearch");
  458 + }
  459 + }
  460 + content = '';
  461 + _ref = this.results_data;
  462 + for (_i = 0, _len = _ref.length; _i < _len; _i++) {
  463 + data = _ref[_i];
  464 + if (data.group) {
  465 + content += this.result_add_group(data);
  466 + } else if (!data.empty) {
  467 + content += this.result_add_option(data);
  468 + if (data.selected && this.is_multiple) {
  469 + this.choice_build(data);
  470 + } else if (data.selected && !this.is_multiple) {
  471 + this.selected_item.find("span").text(data.text);
  472 + if (this.allow_single_deselect) {
  473 + this.single_deselect_control_build();
  474 + }
  475 + }
  476 + }
  477 + }
  478 + this.search_field_disabled();
  479 + this.show_search_field_default();
  480 + this.search_field_scale();
  481 + this.search_results.html(content);
  482 + return this.parsing = false;
  483 + };
  484 + Chosen.prototype.result_add_group = function(group) {
  485 + if (!group.disabled) {
  486 + group.dom_id = this.container_id + "_g_" + group.array_index;
  487 + return '<li id="' + group.dom_id + '" class="group-result">' + $("<div />").text(group.label).html() + '</li>';
  488 + } else {
  489 + return "";
  490 + }
  491 + };
  492 + Chosen.prototype.result_do_highlight = function(el) {
  493 + var high_bottom, high_top, maxHeight, visible_bottom, visible_top;
  494 + if (el.length) {
  495 + this.result_clear_highlight();
  496 + this.result_highlight = el;
  497 + this.result_highlight.addClass("highlighted");
  498 + maxHeight = parseInt(this.search_results.css("maxHeight"), 10);
  499 + visible_top = this.search_results.scrollTop();
  500 + visible_bottom = maxHeight + visible_top;
  501 + high_top = this.result_highlight.position().top + this.search_results.scrollTop();
  502 + high_bottom = high_top + this.result_highlight.outerHeight();
  503 + if (high_bottom >= visible_bottom) {
  504 + return this.search_results.scrollTop((high_bottom - maxHeight) > 0 ? high_bottom - maxHeight : 0);
  505 + } else if (high_top < visible_top) {
  506 + return this.search_results.scrollTop(high_top);
  507 + }
  508 + }
  509 + };
  510 + Chosen.prototype.result_clear_highlight = function() {
  511 + if (this.result_highlight) {
  512 + this.result_highlight.removeClass("highlighted");
  513 + }
  514 + return this.result_highlight = null;
  515 + };
  516 + Chosen.prototype.results_show = function() {
  517 + var dd_top;
  518 + if (!this.is_multiple) {
  519 + this.selected_item.addClass("chzn-single-with-drop");
  520 + if (this.result_single_selected) {
  521 + this.result_do_highlight(this.result_single_selected);
  522 + }
  523 + }
  524 + dd_top = this.is_multiple ? this.container.height() : this.container.height() - 1;
  525 + this.dropdown.css({
  526 + "top": dd_top + "px",
  527 + "left": 0
  528 + });
  529 + this.results_showing = true;
  530 + this.search_field.focus();
  531 + this.search_field.val(this.search_field.val());
  532 + return this.winnow_results();
  533 + };
  534 + Chosen.prototype.results_hide = function() {
  535 + if (!this.is_multiple) {
  536 + this.selected_item.removeClass("chzn-single-with-drop");
  537 + }
  538 + this.result_clear_highlight();
  539 + this.dropdown.css({
  540 + "left": "-9000px"
  541 + });
  542 + return this.results_showing = false;
  543 + };
  544 + Chosen.prototype.set_tab_index = function(el) {
  545 + var ti;
  546 + if (this.form_field_jq.attr("tabindex")) {
  547 + ti = this.form_field_jq.attr("tabindex");
  548 + this.form_field_jq.attr("tabindex", -1);
  549 + if (this.is_multiple) {
  550 + return this.search_field.attr("tabindex", ti);
  551 + } else {
  552 + this.selected_item.attr("tabindex", ti);
  553 + return this.search_field.attr("tabindex", -1);
  554 + }
  555 + }
  556 + };
  557 + Chosen.prototype.show_search_field_default = function() {
  558 + if (this.is_multiple && this.choices < 1 && !this.active_field) {
  559 + this.search_field.val(this.default_text);
  560 + return this.search_field.addClass("default");
  561 + } else {
  562 + this.search_field.val("");
  563 + return this.search_field.removeClass("default");
  564 + }
  565 + };
  566 + Chosen.prototype.search_results_mouseup = function(evt) {
  567 + var target;
  568 + target = $(evt.target).hasClass("active-result") ? $(evt.target) : $(evt.target).parents(".active-result").first();
  569 + if (target.length) {
  570 + this.result_highlight = target;
  571 + return this.result_select(evt);
  572 + }
  573 + };
  574 + Chosen.prototype.search_results_mouseover = function(evt) {
  575 + var target;
  576 + target = $(evt.target).hasClass("active-result") ? $(evt.target) : $(evt.target).parents(".active-result").first();
  577 + if (target) {
  578 + return this.result_do_highlight(target);
  579 + }
  580 + };
  581 + Chosen.prototype.search_results_mouseout = function(evt) {
  582 + if ($(evt.target).hasClass("active-result" || $(evt.target).parents('.active-result').first())) {
  583 + return this.result_clear_highlight();
  584 + }
  585 + };
  586 + Chosen.prototype.choices_click = function(evt) {
  587 + evt.preventDefault();
  588 + if (this.active_field && !($(evt.target).hasClass("search-choice" || $(evt.target).parents('.search-choice').first)) && !this.results_showing) {
  589 + return this.results_show();
  590 + }
  591 + };
  592 + Chosen.prototype.choice_build = function(item) {
  593 + var choice_id, link;
  594 + choice_id = this.container_id + "_c_" + item.array_index;
  595 + this.choices += 1;
  596 + this.search_container.before('<li class="search-choice" id="' + choice_id + '"><span>' + item.html + '</span><a href="javascript:void(0)" class="search-choice-close" rel="' + item.array_index + '"></a></li>');
  597 + link = $('#' + choice_id).find("a").first();
  598 + return link.click(__bind(function(evt) {
  599 + return this.choice_destroy_link_click(evt);
  600 + }, this));
  601 + };
  602 + Chosen.prototype.choice_destroy_link_click = function(evt) {
  603 + evt.preventDefault();
  604 + if (!this.is_disabled) {
  605 + this.pending_destroy_click = true;
  606 + return this.choice_destroy($(evt.target));
  607 + } else {
  608 + return evt.stopPropagation;
  609 + }
  610 + };
  611 + Chosen.prototype.choice_destroy = function(link) {
  612 + this.choices -= 1;
  613 + this.show_search_field_default();
  614 + if (this.is_multiple && this.choices > 0 && this.search_field.val().length < 1) {
  615 + this.results_hide();
  616 + }
  617 + this.result_deselect(link.attr("rel"));
  618 + return link.parents('li').first().remove();
  619 + };
  620 + Chosen.prototype.results_reset = function(evt) {
  621 + this.form_field.options[0].selected = true;
  622 + this.selected_item.find("span").text(this.default_text);
  623 + this.show_search_field_default();
  624 + $(evt.target).remove();
  625 + this.form_field_jq.trigger("change");
  626 + if (this.active_field) {
  627 + return this.results_hide();
  628 + }
  629 + };
  630 + Chosen.prototype.result_select = function(evt) {
  631 + var high, high_id, item, position;
  632 + if (this.result_highlight) {
  633 + high = this.result_highlight;
  634 + high_id = high.attr("id");
  635 + this.result_clear_highlight();
  636 + if (this.is_multiple) {
  637 + this.result_deactivate(high);
  638 + } else {
  639 + this.search_results.find(".result-selected").removeClass("result-selected");
  640 + this.result_single_selected = high;
  641 + }
  642 + high.addClass("result-selected");
  643 + position = high_id.substr(high_id.lastIndexOf("_") + 1);
  644 + item = this.results_data[position];
  645 + item.selected = true;
  646 + this.form_field.options[item.options_index].selected = true;
  647 + if (this.is_multiple) {
  648 + this.choice_build(item);
  649 + } else {
  650 + this.selected_item.find("span").first().text(item.text);
  651 + if (this.allow_single_deselect) {
  652 + this.single_deselect_control_build();
  653 + }
  654 + }
  655 + if (!(evt.metaKey && this.is_multiple)) {
  656 + this.results_hide();
  657 + }
  658 + this.search_field.val("");
  659 + this.form_field_jq.trigger("change");
  660 + return this.search_field_scale();
  661 + }
  662 + };
  663 + Chosen.prototype.result_activate = function(el) {
  664 + return el.addClass("active-result");
  665 + };
  666 + Chosen.prototype.result_deactivate = function(el) {
  667 + return el.removeClass("active-result");
  668 + };
  669 + Chosen.prototype.result_deselect = function(pos) {
  670 + var result, result_data;
  671 + result_data = this.results_data[pos];
  672 + result_data.selected = false;
  673 + this.form_field.options[result_data.options_index].selected = false;
  674 + result = $("#" + this.container_id + "_o_" + pos);
  675 + result.removeClass("result-selected").addClass("active-result").show();
  676 + this.result_clear_highlight();
  677 + this.winnow_results();
  678 + this.form_field_jq.trigger("change");
  679 + return this.search_field_scale();
  680 + };
  681 + Chosen.prototype.single_deselect_control_build = function() {
  682 + if (this.allow_single_deselect && this.selected_item.find("abbr").length < 1) {
  683 + return this.selected_item.find("span").first().after("<abbr class=\"search-choice-close\"></abbr>");
  684 + }
  685 + };
  686 + Chosen.prototype.winnow_results = function() {
  687 + var found, option, part, parts, regex, result_id, results, searchText, startTime, startpos, text, zregex, _i, _j, _len, _len2, _ref;
  688 + startTime = new Date();
  689 + this.no_results_clear();
  690 + results = 0;
  691 + searchText = this.search_field.val() === this.default_text ? "" : $('<div/>').text($.trim(this.search_field.val())).html();
  692 + regex = new RegExp('^' + searchText.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"), 'i');
  693 + zregex = new RegExp(searchText.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"), 'i');
  694 + _ref = this.results_data;
  695 + for (_i = 0, _len = _ref.length; _i < _len; _i++) {
  696 + option = _ref[_i];
  697 + if (!option.disabled && !option.empty) {
  698 + if (option.group) {
  699 + $('#' + option.dom_id).hide();
  700 + } else if (!(this.is_multiple && option.selected)) {
  701 + found = false;
  702 + result_id = option.dom_id;
  703 + if (regex.test(option.html)) {
  704 + found = true;
  705 + results += 1;
  706 + } else if (option.html.indexOf(" ") >= 0 || option.html.indexOf("[") === 0) {
  707 + parts = option.html.replace(/\[|\]/g, "").split(" ");
  708 + if (parts.length) {
  709 + for (_j = 0, _len2 = parts.length; _j < _len2; _j++) {
  710 + part = parts[_j];
  711 + if (regex.test(part)) {
  712 + found = true;
  713 + results += 1;
  714 + }
  715 + }
  716 + }
  717 + }
  718 + if (found) {
  719 + if (searchText.length) {
  720 + startpos = option.html.search(zregex);
  721 + text = option.html.substr(0, startpos + searchText.length) + '</em>' + option.html.substr(startpos + searchText.length);
  722 + text = text.substr(0, startpos) + '<em>' + text.substr(startpos);
  723 + } else {
  724 + text = option.html;
  725 + }
  726 + if ($("#" + result_id).html !== text) {
  727 + $("#" + result_id).html(text);
  728 + }
  729 + this.result_activate($("#" + result_id));
  730 + if (option.group_array_index != null) {
  731 + $("#" + this.results_data[option.group_array_index].dom_id).show();
  732 + }
  733 + } else {
  734 + if (this.result_highlight && result_id === this.result_highlight.attr('id')) {
  735 + this.result_clear_highlight();
  736 + }
  737 + this.result_deactivate($("#" + result_id));
  738 + }
  739 + }
  740 + }
  741 + }
  742 + if (results < 1 && searchText.length) {
  743 + return this.no_results(searchText);
  744 + } else {
  745 + return this.winnow_results_set_highlight();
  746 + }
  747 + };
  748 + Chosen.prototype.winnow_results_clear = function() {
  749 + var li, lis, _i, _len, _results;
  750 + this.search_field.val("");
  751 + lis = this.search_results.find("li");
  752 + _results = [];
  753 + for (_i = 0, _len = lis.length; _i < _len; _i++) {
  754 + li = lis[_i];
  755 + li = $(li);
  756 + _results.push(li.hasClass("group-result") ? li.show() : !this.is_multiple || !li.hasClass("result-selected") ? this.result_activate(li) : void 0);
  757 + }
  758 + return _results;
  759 + };
  760 + Chosen.prototype.winnow_results_set_highlight = function() {
  761 + var do_high, selected_results;
  762 + if (!this.result_highlight) {
  763 + selected_results = !this.is_multiple ? this.search_results.find(".result-selected.active-result") : [];
  764 + do_high = selected_results.length ? selected_results.first() : this.search_results.find(".active-result").first();
  765 + if (do_high != null) {
  766 + return this.result_do_highlight(do_high);
  767 + }
  768 + }
  769 + };
  770 + Chosen.prototype.no_results = function(terms) {
  771 + var no_results_html;
  772 + no_results_html = $('<li class="no-results">' + this.results_none_found + ' "<span></span>"</li>');
  773 + no_results_html.find("span").first().html(terms);
  774 + return this.search_results.append(no_results_html);
  775 + };
  776 + Chosen.prototype.no_results_clear = function() {
  777 + return this.search_results.find(".no-results").remove();
  778 + };
  779 + Chosen.prototype.keydown_arrow = function() {
  780 + var first_active, next_sib;
  781 + if (!this.result_highlight) {
  782 + first_active = this.search_results.find("li.active-result").first();
  783 + if (first_active) {
  784 + this.result_do_highlight($(first_active));
  785 + }
  786 + } else if (this.results_showing) {
  787 + next_sib = this.result_highlight.nextAll("li.active-result").first();
  788 + if (next_sib) {
  789 + this.result_do_highlight(next_sib);
  790 + }
  791 + }
  792 + if (!this.results_showing) {
  793 + return this.results_show();
  794 + }
  795 + };
  796 + Chosen.prototype.keyup_arrow = function() {
  797 + var prev_sibs;
  798 + if (!this.results_showing && !this.is_multiple) {
  799 + return this.results_show();
  800 + } else if (this.result_highlight) {
  801 + prev_sibs = this.result_highlight.prevAll("li.active-result");
  802 + if (prev_sibs.length) {
  803 + return this.result_do_highlight(prev_sibs.first());
  804 + } else {
  805 + if (this.choices > 0) {
  806 + this.results_hide();
  807 + }
  808 + return this.result_clear_highlight();
  809 + }
  810 + }
  811 + };
  812 + Chosen.prototype.keydown_backstroke = function() {
  813 + if (this.pending_backstroke) {
  814 + this.choice_destroy(this.pending_backstroke.find("a").first());
  815 + return this.clear_backstroke();
  816 + } else {
  817 + this.pending_backstroke = this.search_container.siblings("li.search-choice").last();
  818 + return this.pending_backstroke.addClass("search-choice-focus");
  819 + }
  820 + };
  821 + Chosen.prototype.clear_backstroke = function() {
  822 + if (this.pending_backstroke) {
  823 + this.pending_backstroke.removeClass("search-choice-focus");
  824 + }
  825 + return this.pending_backstroke = null;
  826 + };
  827 + Chosen.prototype.keydown_checker = function(evt) {
  828 + var stroke, _ref;
  829 + stroke = (_ref = evt.which) != null ? _ref : evt.keyCode;
  830 + this.search_field_scale();
  831 + if (stroke !== 8 && this.pending_backstroke) {
  832 + this.clear_backstroke();
  833 + }
  834 + switch (stroke) {
  835 + case 8:
  836 + this.backstroke_length = this.search_field.val().length;
  837 + break;
  838 + case 9:
  839 + if (this.results_showing && !this.is_multiple) {
  840 + this.result_select(evt);
  841 + }
  842 + this.mouse_on_container = false;
  843 + break;
  844 + case 13:
  845 + evt.preventDefault();
  846 + break;
  847 + case 38:
  848 + evt.preventDefault();
  849 + this.keyup_arrow();
  850 + break;
  851 + case 40:
  852 + this.keydown_arrow();
  853 + break;
  854 + }
  855 + };
  856 + Chosen.prototype.search_field_scale = function() {
  857 + var dd_top, div, h, style, style_block, styles, w, _i, _len;
  858 + if (this.is_multiple) {
  859 + h = 0;
  860 + w = 0;
  861 + style_block = "position:absolute; left: -1000px; top: -1000px; display:none;";
  862 + styles = ['font-size', 'font-style', 'font-weight', 'font-family', 'line-height', 'text-transform', 'letter-spacing'];
  863 + for (_i = 0, _len = styles.length; _i < _len; _i++) {
  864 + style = styles[_i];
  865 + style_block += style + ":" + this.search_field.css(style) + ";";
  866 + }
  867 + div = $('<div />', {
  868 + 'style': style_block
  869 + });
  870 + div.text(this.search_field.val());
  871 + $('body').append(div);
  872 + w = div.width() + 25;
  873 + div.remove();
  874 + if (w > this.f_width - 10) {
  875 + w = this.f_width - 10;
  876 + }
  877 + this.search_field.css({
  878 + 'width': w + 'px'
  879 + });
  880 + dd_top = this.container.height();
  881 + return this.dropdown.css({
  882 + "top": dd_top + "px"
  883 + });
  884 + }
  885 + };
  886 + Chosen.prototype.generate_random_id = function() {
  887 + var string;
  888 + string = "sel" + this.generate_random_char() + this.generate_random_char() + this.generate_random_char();
  889 + while ($("#" + string).length > 0) {
  890 + string += this.generate_random_char();
  891 + }
  892 + return string;
  893 + };
  894 + return Chosen;
  895 + })();
  896 + get_side_border_padding = function(elmt) {
  897 + var side_border_padding;
  898 + return side_border_padding = elmt.outerWidth() - elmt.width();
  899 + };
  900 + root.get_side_border_padding = get_side_border_padding;
  901 +}).call(this);
... ...
app/assets/javascripts/commits.js
... ... @@ -7,3 +7,51 @@ $(document).ready(function(){
7 7 }
8 8 });
9 9 });
  10 +
  11 +
  12 +
  13 +var CommitsList = {
  14 +
  15 +ref:null,
  16 +limit:0,
  17 +offset:0,
  18 +
  19 +init:
  20 + function(ref, limit) {
  21 + this.ref=ref;
  22 + this.limit=limit;
  23 + this.offset=limit;
  24 + this.initLoadMore();
  25 + $('.loading').show();
  26 + },
  27 +
  28 +getOld:
  29 + function() {
  30 + $('.loading').show();
  31 + $.ajax({
  32 + type: "GET",
  33 + url: location.href,
  34 + data: "limit=" + this.limit + "&offset=" + this.offset + "&ref=" + this.ref,
  35 + complete: function(){ $('.loading').hide()},
  36 + dataType: "script"});
  37 + },
  38 +
  39 +append:
  40 + function(count, html) {
  41 + $("#commits_list").append(html);
  42 + if(count > 0) {
  43 + this.offset += count;
  44 + this.initLoadMore();
  45 + }
  46 + },
  47 +
  48 +initLoadMore:
  49 + function() {
  50 + $(window).bind('scroll', function(){
  51 + if($(window).scrollTop() == $(document).height() - $(window).height()){
  52 + $(window).unbind('scroll');
  53 + CommitsList.getOld();
  54 + }
  55 + });
  56 + }
  57 +}
... ...
app/assets/javascripts/note.js 0 → 100644
... ... @@ -0,0 +1,87 @@
  1 +var NoteList = {
  2 +
  3 +first_id: 0,
  4 +last_id: 0,
  5 +resource_name: null,
  6 +
  7 +init:
  8 + function(resource_name, first_id, last_id) {
  9 + this.resource_name = resource_name;
  10 + this.first_id = first_id;
  11 + this.last_id = last_id;
  12 + this.initRefresh();
  13 + this.initLoadMore();
  14 + },
  15 +
  16 +getOld:
  17 + function() {
  18 + $('.loading').show();
  19 + $.ajax({
  20 + type: "GET",
  21 + url: location.href,
  22 + data: "first_id=" + this.first_id,
  23 + complete: function(){ $('.loading').hide()},
  24 + dataType: "script"});
  25 + },
  26 +
  27 +append:
  28 + function(id, html) {
  29 + this.first_id = id;
  30 + $("#notes-list").append(html);
  31 + this.initLoadMore();
  32 + },
  33 +
  34 +replace:
  35 + function(fid, lid, html) {
  36 + this.first_id = fid;
  37 + this.last_id = lid;
  38 + $("#notes-list").html(html);
  39 + this.initLoadMore();
  40 + },
  41 +
  42 +
  43 +prepend:
  44 + function(id, html) {
  45 + this.last_id = id;
  46 + $("#notes-list").prepend(html);
  47 + },
  48 +
  49 +getNew:
  50 + function() {
  51 + // refersh notes list
  52 + $.ajax({
  53 + type: "GET",
  54 + url: location.href,
  55 + data: "last_id=" + this.last_id,
  56 + dataType: "script"});
  57 + },
  58 +
  59 +refresh:
  60 + function() {
  61 + // refersh notes list
  62 + $.ajax({
  63 + type: "GET",
  64 + url: location.href,
  65 + data: "first_id=" + this.first_id + "&last_id=" + this.last_id,
  66 + dataType: "script"});
  67 + },
  68 +
  69 +
  70 +
  71 +initRefresh:
  72 + function() {
  73 + // init timer
  74 + var intNew = setInterval("NoteList.getNew()", 15000);
  75 + var intRefresh = setInterval("NoteList.refresh()", 90000);
  76 + },
  77 +
  78 +initLoadMore:
  79 + function() {
  80 + $(window).bind('scroll', function(){
  81 + if($(window).scrollTop() == $(document).height() - $(window).height()){
  82 + $(window).unbind('scroll');
  83 + NoteList.getOld();
  84 + }
  85 + });
  86 + }
  87 +}
... ...
app/assets/stylesheets/application.css
... ... @@ -4,6 +4,7 @@
4 4 * the top of the compiled file, but it's generally better to create a new file per style scope.
5 5 *= require jquery-ui/jquery-ui
6 6 *= require jquery-ui/jquery.ui.selectmenu
  7 + *= require jquery-ui/jquery.tagify
7 8 *= require_self
8 9 *= require_tree .
9 10 */
... ...
app/assets/stylesheets/chosen.css 0 → 100644
... ... @@ -0,0 +1,367 @@
  1 +/* @group Base */
  2 +.chzn-container {
  3 + font-size: 13px;
  4 + position: relative;
  5 + display: inline-block;
  6 + zoom: 1;
  7 + *display: inline;
  8 +}
  9 +.chzn-container .chzn-drop {
  10 + background: #fff;
  11 + border: 1px solid #aaa;
  12 + border-top: 0;
  13 + position: absolute;
  14 + top: 29px;
  15 + left: 0;
  16 + -webkit-box-shadow: 0 4px 5px rgba(0,0,0,.15);
  17 + -moz-box-shadow : 0 4px 5px rgba(0,0,0,.15);
  18 + -o-box-shadow : 0 4px 5px rgba(0,0,0,.15);
  19 + box-shadow : 0 4px 5px rgba(0,0,0,.15);
  20 + z-index: 999;
  21 +}
  22 +/* @end */
  23 +
  24 +/* @group Single Chosen */
  25 +.chzn-container-single .chzn-single {
  26 + background-color: #fff;
  27 + background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #eeeeee), color-stop(0.5, white));
  28 + background-image: -webkit-linear-gradient(center bottom, #eeeeee 0%, white 50%);
  29 + background-image: -moz-linear-gradient(center bottom, #eeeeee 0%, white 50%);
  30 + background-image: -o-linear-gradient(top, #eeeeee 0%,#ffffff 50%);
  31 + background-image: -ms-linear-gradient(top, #eeeeee 0%,#ffffff 50%);
  32 + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#eeeeee', endColorstr='#ffffff',GradientType=0 );
  33 + background-image: linear-gradient(top, #eeeeee 0%,#ffffff 50%);
  34 + -webkit-border-radius: 4px;
  35 + -moz-border-radius : 4px;
  36 + border-radius : 4px;
  37 + -moz-background-clip : padding;
  38 + -webkit-background-clip: padding-box;
  39 + background-clip : padding-box;
  40 + border: 1px solid #aaa;
  41 + display: block;
  42 + overflow: hidden;
  43 + white-space: nowrap;
  44 + position: relative;
  45 + height: 26px;
  46 + line-height: 26px;
  47 + padding: 0 0 0 8px;
  48 + color: #444;
  49 + text-decoration: none;
  50 +}
  51 +.chzn-container-single .chzn-single span {
  52 + margin-right: 26px;
  53 + display: block;
  54 + overflow: hidden;
  55 + white-space: nowrap;
  56 + -o-text-overflow: ellipsis;
  57 + -ms-text-overflow: ellipsis;
  58 + text-overflow: ellipsis;
  59 +}
  60 +.chzn-container-single .chzn-single abbr {
  61 + display: block;
  62 + position: absolute;
  63 + right: 26px;
  64 + top: 8px;
  65 + width: 12px;
  66 + height: 13px;
  67 + font-size: 1px;
  68 + background: url(chosen-sprite.png) right top no-repeat;
  69 +}
  70 +.chzn-container-single .chzn-single abbr:hover {
  71 + background-position: right -11px;
  72 +}
  73 +.chzn-container-single .chzn-single div {
  74 + -webkit-border-radius: 0 4px 4px 0;
  75 + -moz-border-radius : 0 4px 4px 0;
  76 + border-radius : 0 4px 4px 0;
  77 + -moz-background-clip : padding;
  78 + -webkit-background-clip: padding-box;
  79 + background-clip : padding-box;
  80 + background: #ccc;
  81 + background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #ccc), color-stop(0.6, #eee));
  82 + background-image: -webkit-linear-gradient(center bottom, #ccc 0%, #eee 60%);
  83 + background-image: -moz-linear-gradient(center bottom, #ccc 0%, #eee 60%);
  84 + background-image: -o-linear-gradient(bottom, #ccc 0%, #eee 60%);
  85 + background-image: -ms-linear-gradient(top, #cccccc 0%,#eeeeee 60%);
  86 + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#cccccc', endColorstr='#eeeeee',GradientType=0 );
  87 + background-image: linear-gradient(top, #cccccc 0%,#eeeeee 60%);
  88 + border-left: 1px solid #aaa;
  89 + position: absolute;
  90 + right: 0;
  91 + top: 0;
  92 + display: block;
  93 + height: 100%;
  94 + width: 18px;
  95 +}
  96 +.chzn-container-single .chzn-single div b {
  97 + background: url('chosen-sprite.png') no-repeat 0 1px;
  98 + display: block;
  99 + width: 100%;
  100 + height: 100%;
  101 +}
  102 +.chzn-container-single .chzn-search {
  103 + padding: 3px 4px;
  104 + position: relative;
  105 + margin: 0;
  106 + white-space: nowrap;
  107 + z-index: 1010;
  108 +}
  109 +.chzn-container-single .chzn-search input {
  110 + background: #fff url('chosen-sprite.png') no-repeat 100% -22px;
  111 + background: url('chosen-sprite.png') no-repeat 100% -22px, -webkit-gradient(linear, left bottom, left top, color-stop(0.85, white), color-stop(0.99, #eeeeee));
  112 + background: url('chosen-sprite.png') no-repeat 100% -22px, -webkit-linear-gradient(center bottom, white 85%, #eeeeee 99%);
  113 + background: url('chosen-sprite.png') no-repeat 100% -22px, -moz-linear-gradient(center bottom, white 85%, #eeeeee 99%);
  114 + background: url('chosen-sprite.png') no-repeat 100% -22px, -o-linear-gradient(bottom, white 85%, #eeeeee 99%);
  115 + background: url('chosen-sprite.png') no-repeat 100% -22px, -ms-linear-gradient(top, #ffffff 85%,#eeeeee 99%);
  116 + background: url('chosen-sprite.png') no-repeat 100% -22px, linear-gradient(top, #ffffff 85%,#eeeeee 99%);
  117 + margin: 1px 0;
  118 + padding: 4px 20px 4px 5px;
  119 + outline: 0;
  120 + border: 1px solid #aaa;
  121 + font-family: sans-serif;
  122 + font-size: 1em;
  123 +}
  124 +.chzn-container-single .chzn-drop {
  125 + -webkit-border-radius: 0 0 4px 4px;
  126 + -moz-border-radius : 0 0 4px 4px;
  127 + border-radius : 0 0 4px 4px;
  128 + -moz-background-clip : padding;
  129 + -webkit-background-clip: padding-box;
  130 + background-clip : padding-box;
  131 +}
  132 +/* @end */
  133 +
  134 +.chzn-container-single-nosearch .chzn-search input {
  135 + position: absolute;
  136 + left: -9000px;
  137 +}
  138 +
  139 +/* @group Multi Chosen */
  140 +.chzn-container-multi .chzn-choices {
  141 + background-color: #fff;
  142 + background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0.85, white), color-stop(0.99, #eeeeee));
  143 + background-image: -webkit-linear-gradient(center bottom, white 85%, #eeeeee 99%);
  144 + background-image: -moz-linear-gradient(center bottom, white 85%, #eeeeee 99%);
  145 + background-image: -o-linear-gradient(bottom, white 85%, #eeeeee 99%);
  146 + background-image: -ms-linear-gradient(top, #ffffff 85%,#eeeeee 99%);
  147 + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffff', endColorstr='#eeeeee',GradientType=0 );
  148 + background-image: linear-gradient(top, #ffffff 85%,#eeeeee 99%);
  149 + border: 1px solid #aaa;
  150 + margin: 0;
  151 + padding: 0;
  152 + cursor: text;
  153 + overflow: hidden;
  154 + height: auto !important;
  155 + height: 1%;
  156 + position: relative;
  157 +}
  158 +.chzn-container-multi .chzn-choices li {
  159 + float: left;
  160 + list-style: none;
  161 +}
  162 +.chzn-container-multi .chzn-choices .search-field {
  163 + white-space: nowrap;
  164 + margin: 0;
  165 + padding: 0;
  166 +}
  167 +.chzn-container-multi .chzn-choices .search-field input {
  168 + color: #666;
  169 + background: transparent !important;
  170 + border: 0 !important;
  171 + padding: 5px;
  172 + margin: 1px 0;
  173 + outline: 0;
  174 + -webkit-box-shadow: none;
  175 + -moz-box-shadow : none;
  176 + -o-box-shadow : none;
  177 + box-shadow : none;
  178 +}
  179 +.chzn-container-multi .chzn-choices .search-field .default {
  180 + color: #999;
  181 +}
  182 +.chzn-container-multi .chzn-choices .search-choice {
  183 + -webkit-border-radius: 3px;
  184 + -moz-border-radius : 3px;
  185 + border-radius : 3px;
  186 + -moz-background-clip : padding;
  187 + -webkit-background-clip: padding-box;
  188 + background-clip : padding-box;
  189 + background-color: #e4e4e4;
  190 + background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #e4e4e4), color-stop(0.7, #eeeeee));
  191 + background-image: -webkit-linear-gradient(center bottom, #e4e4e4 0%, #eeeeee 70%);
  192 + background-image: -moz-linear-gradient(center bottom, #e4e4e4 0%, #eeeeee 70%);
  193 + background-image: -o-linear-gradient(bottom, #e4e4e4 0%, #eeeeee 70%);
  194 + background-image: -ms-linear-gradient(top, #e4e4e4 0%,#eeeeee 70%);
  195 + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#e4e4e4', endColorstr='#eeeeee',GradientType=0 );
  196 + background-image: linear-gradient(top, #e4e4e4 0%,#eeeeee 70%);
  197 + color: #333;
  198 + border: 1px solid #b4b4b4;
  199 + line-height: 13px;
  200 + padding: 3px 19px 3px 6px;
  201 + margin: 3px 0 3px 5px;
  202 + position: relative;
  203 +}
  204 +.chzn-container-multi .chzn-choices .search-choice span {
  205 + cursor: default;
  206 +}
  207 +.chzn-container-multi .chzn-choices .search-choice-focus {
  208 + background: #d4d4d4;
  209 +}
  210 +.chzn-container-multi .chzn-choices .search-choice .search-choice-close {
  211 + display: block;
  212 + position: absolute;
  213 + right: 3px;
  214 + top: 4px;
  215 + width: 12px;
  216 + height: 13px;
  217 + font-size: 1px;
  218 + background: url(chosen-sprite.png) right top no-repeat;
  219 +}
  220 +.chzn-container-multi .chzn-choices .search-choice .search-choice-close:hover {
  221 + background-position: right -11px;
  222 +}
  223 +.chzn-container-multi .chzn-choices .search-choice-focus .search-choice-close {
  224 + background-position: right -11px;
  225 +}
  226 +/* @end */
  227 +
  228 +/* @group Results */
  229 +.chzn-container .chzn-results {
  230 + margin: 0 4px 4px 0;
  231 + max-height: 190px;
  232 + padding: 0 0 0 4px;
  233 + position: relative;
  234 + overflow-x: hidden;
  235 + overflow-y: auto;
  236 +}
  237 +.chzn-container-multi .chzn-results {
  238 + margin: -1px 0 0;
  239 + padding: 0;
  240 +}
  241 +.chzn-container .chzn-results li {
  242 + display: none;
  243 + line-height: 80%;
  244 + padding: 7px 7px 8px;
  245 + margin: 0;
  246 + list-style: none;
  247 +}
  248 +.chzn-container .chzn-results .active-result {
  249 + cursor: pointer;
  250 + display: list-item;
  251 +}
  252 +.chzn-container .chzn-results .highlighted {
  253 + background: #3875d7;
  254 + color: #fff;
  255 +}
  256 +.chzn-container .chzn-results li em {
  257 + background: #feffde;
  258 + font-style: normal;
  259 +}
  260 +.chzn-container .chzn-results .highlighted em {
  261 + background: transparent;
  262 +}
  263 +.chzn-container .chzn-results .no-results {
  264 + background: #f4f4f4;
  265 + display: list-item;
  266 +}
  267 +.chzn-container .chzn-results .group-result {
  268 + cursor: default;
  269 + color: #999;
  270 + font-weight: bold;
  271 +}
  272 +.chzn-container .chzn-results .group-option {
  273 + padding-left: 20px;
  274 +}
  275 +.chzn-container-multi .chzn-drop .result-selected {
  276 + display: none;
  277 +}
  278 +/* @end */
  279 +
  280 +/* @group Active */
  281 +.chzn-container-active .chzn-single {
  282 + -webkit-box-shadow: 0 0 5px rgba(0,0,0,.3);
  283 + -moz-box-shadow : 0 0 5px rgba(0,0,0,.3);
  284 + -o-box-shadow : 0 0 5px rgba(0,0,0,.3);
  285 + box-shadow : 0 0 5px rgba(0,0,0,.3);
  286 + border: 1px solid #5897fb;
  287 +}
  288 +.chzn-container-active .chzn-single-with-drop {
  289 + border: 1px solid #aaa;
  290 + -webkit-box-shadow: 0 1px 0 #fff inset;
  291 + -moz-box-shadow : 0 1px 0 #fff inset;
  292 + -o-box-shadow : 0 1px 0 #fff inset;
  293 + box-shadow : 0 1px 0 #fff inset;
  294 + background-color: #eee;
  295 + background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, white), color-stop(0.5, #eeeeee));
  296 + background-image: -webkit-linear-gradient(center bottom, white 0%, #eeeeee 50%);
  297 + background-image: -moz-linear-gradient(center bottom, white 0%, #eeeeee 50%);
  298 + background-image: -o-linear-gradient(bottom, white 0%, #eeeeee 50%);
  299 + background-image: -ms-linear-gradient(top, #ffffff 0%,#eeeeee 50%);
  300 + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffff', endColorstr='#eeeeee',GradientType=0 );
  301 + background-image: linear-gradient(top, #ffffff 0%,#eeeeee 50%);
  302 + -webkit-border-bottom-left-radius : 0;
  303 + -webkit-border-bottom-right-radius: 0;
  304 + -moz-border-radius-bottomleft : 0;
  305 + -moz-border-radius-bottomright: 0;
  306 + border-bottom-left-radius : 0;
  307 + border-bottom-right-radius: 0;
  308 +}
  309 +.chzn-container-active .chzn-single-with-drop div {
  310 + background: transparent;
  311 + border-left: none;
  312 +}
  313 +.chzn-container-active .chzn-single-with-drop div b {
  314 + background-position: -18px 1px;
  315 +}
  316 +.chzn-container-active .chzn-choices {
  317 + -webkit-box-shadow: 0 0 5px rgba(0,0,0,.3);
  318 + -moz-box-shadow : 0 0 5px rgba(0,0,0,.3);
  319 + -o-box-shadow : 0 0 5px rgba(0,0,0,.3);
  320 + box-shadow : 0 0 5px rgba(0,0,0,.3);
  321 + border: 1px solid #5897fb;
  322 +}
  323 +.chzn-container-active .chzn-choices .search-field input {
  324 + color: #111 !important;
  325 +}
  326 +/* @end */
  327 +
  328 +/* @group Disabled Support */
  329 +.chzn-disabled {
  330 + cursor: default;
  331 + opacity:0.5 !important;
  332 +}
  333 +.chzn-disabled .chzn-single {
  334 + cursor: default;
  335 +}
  336 +.chzn-disabled .chzn-choices .search-choice .search-choice-close {
  337 + cursor: default;
  338 +}
  339 +
  340 +/* @group Right to Left */
  341 +.chzn-rtl { direction:rtl;text-align: right; }
  342 +.chzn-rtl .chzn-single { padding-left: 0; padding-right: 8px; }
  343 +.chzn-rtl .chzn-single span { margin-left: 26px; margin-right: 0; }
  344 +.chzn-rtl .chzn-single div {
  345 + left: 0; right: auto;
  346 + border-left: none; border-right: 1px solid #aaaaaa;
  347 + -webkit-border-radius: 4px 0 0 4px;
  348 + -moz-border-radius : 4px 0 0 4px;
  349 + border-radius : 4px 0 0 4px;
  350 +}
  351 +.chzn-rtl .chzn-choices li { float: right; }
  352 +.chzn-rtl .chzn-choices .search-choice { padding: 3px 6px 3px 19px; margin: 3px 5px 3px 0; }
  353 +.chzn-rtl .chzn-choices .search-choice .search-choice-close { left: 5px; right: auto; background-position: right top;}
  354 +.chzn-rtl.chzn-container-single .chzn-results { margin-left: 4px; margin-right: 0; padding-left: 0; padding-right: 4px; }
  355 +.chzn-rtl .chzn-results .group-option { padding-left: 0; padding-right: 20px; }
  356 +.chzn-rtl.chzn-container-active .chzn-single-with-drop div { border-right: none; }
  357 +.chzn-rtl .chzn-search input {
  358 + background: url('chosen-sprite.png') no-repeat -38px -22px, #ffffff;
  359 + background: url('chosen-sprite.png') no-repeat -38px -22px, -webkit-gradient(linear, left bottom, left top, color-stop(0.85, white), color-stop(0.99, #eeeeee));
  360 + background: url('chosen-sprite.png') no-repeat -38px -22px, -webkit-linear-gradient(center bottom, white 85%, #eeeeee 99%);
  361 + background: url('chosen-sprite.png') no-repeat -38px -22px, -moz-linear-gradient(center bottom, white 85%, #eeeeee 99%);
  362 + background: url('chosen-sprite.png') no-repeat -38px -22px, -o-linear-gradient(bottom, white 85%, #eeeeee 99%);
  363 + background: url('chosen-sprite.png') no-repeat -38px -22px, -ms-linear-gradient(top, #ffffff 85%,#eeeeee 99%);
  364 + background: url('chosen-sprite.png') no-repeat -38px -22px, linear-gradient(top, #ffffff 85%,#eeeeee 99%);
  365 + padding: 4px 5px 4px 20px;
  366 +}
  367 +/* @end */
... ...
app/assets/stylesheets/projects.css.scss
... ... @@ -52,7 +52,8 @@
52 52 background: #eee;
53 53 }
54 54 .diff_file_content {
55   - overflow-x: scroll;
  55 + overflow:auto;
  56 + overflow-y:hidden;
56 57 background:#fff;
57 58 color:#333;
58 59 font-size: 12px;
... ... @@ -162,10 +163,6 @@ table.round-borders {
162 163 padding:20px;
163 164 }
164 165  
165   -//body {
166   - //background: #eaeaea;
167   -//}
168   -
169 166 a {
170 167 color: #111;
171 168 }
... ... @@ -174,9 +171,9 @@ a {
174 171 .old_line, .new_line {
175 172 background:#ECECEC;
176 173 color:#777;
177   - width:15px;
  174 + width:30px;
178 175 float:left;
179   - padding: 0px 10px;
  176 + padding: 0px 5px;
180 177 border-right: 1px solid #ccc;
181 178 }
182 179 }
... ... @@ -231,43 +228,15 @@ input.ssh_project_url {
231 228 text-align:center;
232 229 }
233 230  
234   -.day-commits-table li.commit {
235   - cursor:pointer;
236   -
237   - &:hover {
238   - @include hover-color;
239   - }
240   -}
241   -
242   -/*
243   -#FFF6BF
244   -#FFD324
245   -*/
246   -#tree-slider tr.tree-item {
247   - cursor:pointer;
248   -
249   - &:hover {
250   - @include hover-color;
251   - td {
252   - @include hover-color;
253   - }
254   - }
255   -}
256 231 #projects-list .project {
257 232 height:50px;
258 233 }
259 234  
  235 +#tree-slider .tree-item,
260 236 #projects-list .project,
261 237 #snippets-table .snippet,
262 238 #issues-table .issue{
263 239 cursor:pointer;
264   -
265   - &:hover {
266   - @include hover-color;
267   - td {
268   - @include hover-color;
269   - }
270   - }
271 240 }
272 241  
273 242 .clear {
... ... @@ -421,31 +390,6 @@ input.ssh_project_url {
421 390 list-style:none;
422 391 margin:0px;
423 392 padding:0px;
424   -
425   - li {
426   - display:list-item;
427   - padding:8px;
428   - margin:0px;
429   - background: #F7FBFC;
430   - border-top: 1px solid #E2EAEE;
431   -
432   - &:first-child {
433   - border-top: none;
434   - }
435   - &:nth-child(2n+1) {
436   - background: white;
437   - }
438   - p {
439   - margin-bottom: 4px;
440   - font-size: 13px;
441   - color:#111;
442   - }
443   - }
444   - cite {
445   - &.ago {
446   - color:#666;
447   - }
448   - }
449 393 }
450 394  
451 395 .notes_count {
... ... @@ -460,14 +404,6 @@ input.ssh_project_url {
460 404 right: 6px;
461 405 top: 6px;
462 406 }
463   -.note_author {
464   - float:left;
465   - width:60px;
466   -}
467   -.note_content {
468   - float:left;
469   - width:650px;
470   -}
471 407  
472 408 .issue_notes {
473 409 .note_content {
... ... @@ -556,8 +492,7 @@ input.ssh_project_url {
556 492  
557 493 }
558 494 .commit,
559   -.message,
560   -#notes-list{
  495 +.message{
561 496 .author {
562 497 background: #eaeaea;
563 498 color: #333;
... ... @@ -574,7 +509,7 @@ input.ssh_project_url {
574 509 font-size:14px;
575 510 }
576 511  
577   -.wall_page {
  512 +#new_note {
578 513 #note_note {
579 514 height:25px;
580 515 }
... ... @@ -596,6 +531,7 @@ input.ssh_project_url {
596 531 @include round-borders-all(4px);
597 532 padding:2px 4px;
598 533 border:none;
  534 + text-shadow:none;
599 535  
600 536 &.high {
601 537 background: #D12F19;
... ... @@ -706,3 +642,44 @@ table.highlighttable pre{
706 642 line-height:16px !important;
707 643 font-size:12px !important;
708 644 }
  645 +
  646 +.project-refs-form {
  647 + span {
  648 + background: none !important;
  649 + position:static !important;
  650 + width:auto !important;
  651 + height: auto !important;
  652 + }
  653 +}
  654 +
  655 +.project-refs-select {
  656 + width:200px;
  657 +}
  658 +
  659 +.issues_filter {
  660 + margin-top:10px;
  661 + .left {
  662 + margin-right:15px;
  663 + }
  664 +}
  665 +
  666 +.cgray { color:gray; }
  667 +.cred { color:#D12F19; }
  668 +.cgreen { color:#44aa22; }
  669 +
  670 +body.project-page table .commit {
  671 + a.tree-commit-link {
  672 + color:gray;
  673 + &:hover {
  674 + text-decoration:underline;
  675 + }
  676 + }
  677 +}
  678 +
  679 +body.project-page #notes-list .note {padding: 10px; border-bottom: 1px solid #eee; overflow: hidden; display: block;}
  680 +body.project-page #notes-list .note {padding: 10px; border-bottom: 1px solid #eee; overflow: hidden; display: block;}
  681 +body.project-page #notes-list .note img{float: left; margin-right: 10px;}
  682 +body.project-page #notes-list .note span.note-title{display: block;}
  683 +body.project-page #notes-list .note span.note-title{margin-bottom: 10px}
  684 +body.project-page #notes-list .note span.note-author{color: #999; font-weight: normal; font-style: italic;}
  685 +body.project-page #notes-list .note span.note-author strong{font-weight: bold; font-style: normal;}
... ...
app/assets/stylesheets/style.scss
... ... @@ -292,7 +292,7 @@ body.login-page{background-color: #f1f1f1; padding-top: 10%}
292 292  
293 293 /* General */
294 294 #container{background-color: white; overflow: hidden;}
295   -/*#container{margin: auto; width: 980px; border: 1px solid rgba(0,0,0,.22); border-top: 0; box-shadow: 0 0 0px 4px rgba(0,0,0,.04)}*/
  295 +body.collapsed #container{margin: auto; width: 980px; border: 1px solid rgba(0,0,0,.22); border-top: 0; box-shadow: 0 0 0px 4px rgba(0,0,0,.04)}
296 296  
297 297  
298 298  
... ... @@ -323,6 +323,7 @@ header nav{border-radius: 4px; box-shadow: 0 1px 2px black; width: 294px; margin
323 323 background-image: -moz-linear-gradient(#595d63 6.6%, #31363e);
324 324 background-image: -o-linear-gradient(#595d63 6.6%, #31363e);
325 325 margin-top: 2px;
  326 + height:30px
326 327 }
327 328 header nav a{padding: 8px 12px 8px 34px; display: inline-block; color: #D6DADF; border-right: 1px solid #31363E; position: relative; box-shadow: 1px 0 0 rgba(255,255,255,.1); margin: 0}
328 329 header nav a span{width: 20px; height: 20px; display: inline-block; background: red; position: absolute; left: 8px; top: 6px;}
... ...
app/assets/stylesheets/tags.css.css 0 → 100644
... ... @@ -0,0 +1,30 @@
  1 +.tags-list {
  2 + padding : 0px 10px 10px 10px;
  3 +
  4 +}
  5 +
  6 +.tags-list a {
  7 + display: inline-block;
  8 + padding: 8px 11px 8px 11px;
  9 + margin: 1px 5px 0px 0px;
  10 + border-radius: 4px;
  11 + border: 1px solid #72bbdf;
  12 + background-color: #72bbdf;
  13 + color: #0f326d;
  14 + font-weight: bold;
  15 + font-size: 14px;
  16 +}
  17 +
  18 +
  19 +.small-tags a{
  20 + font-size: 9px;
  21 +
  22 + display: inline-block;
  23 + padding: 2px 3px 1px 3px;
  24 + margin: 0px 3px 0px 0px;
  25 + border-radius: 2px;
  26 + background-color: #72bbdf;
  27 + color: #FFF;
  28 + text-shadow: none;
  29 + font-weight: bold;
  30 +}
0 31 \ No newline at end of file
... ...
app/controllers/application_controller.rb
1 1 class ApplicationController < ActionController::Base
2 2 before_filter :authenticate_user!
  3 + before_filter :view_style
  4 +
3 5 protect_from_forgery
4 6  
5 7 helper_method :abilities, :can?
... ... @@ -57,19 +59,13 @@ class ApplicationController &lt; ActionController::Base
57 59 end
58 60  
59 61 def load_refs
60   - @branch = unless params[:branch].blank?
61   - params[:branch]
62   - else
63   - nil
64   - end
65   -
66   - @tag = unless params[:tag].blank?
67   - params[:tag]
68   - else
69   - nil
70   - end
71   -
72   - @ref = @branch || @tag || "master"
  62 + unless params[:ref].blank?
  63 + @ref = params[:ref]
  64 + else
  65 + @branch = params[:branch].blank? ? nil : params[:branch]
  66 + @tag = params[:tag].blank? ? nil : params[:tag]
  67 + @ref = @branch || @tag || "master"
  68 + end
73 69 end
74 70  
75 71 def render_404
... ... @@ -79,4 +75,30 @@ class ApplicationController &lt; ActionController::Base
79 75 def require_non_empty_project
80 76 redirect_to @project unless @project.repo_exists?
81 77 end
  78 +
  79 + def view_style
  80 + if params[:view_style] == "collapsed"
  81 + cookies[:view_style] = "collapsed"
  82 + elsif params[:view_style] == "fluid"
  83 + cookies[:view_style] = ""
  84 + end
  85 +
  86 + @view_mode = if cookies[:view_style] == "collapsed"
  87 + :fixed
  88 + else
  89 + :fluid
  90 + end
  91 + end
  92 +
  93 + def respond_with_notes
  94 + if params[:last_id] && params[:first_id]
  95 + @notes = @notes.where("id >= ?", params[:first_id])
  96 + elsif params[:last_id]
  97 + @notes = @notes.where("id > ?", params[:last_id])
  98 + elsif params[:first_id]
  99 + @notes = @notes.where("id < ?", params[:first_id])
  100 + else
  101 + nil
  102 + end
  103 + end
82 104 end
... ...
app/controllers/commits_controller.rb
... ... @@ -13,11 +13,12 @@ class CommitsController &lt; ApplicationController
13 13 load_refs # load @branch, @tag & @ref
14 14  
15 15 @repo = project.repo
  16 + limit, offset = (params[:limit] || 20), (params[:offset] || 0)
16 17  
17 18 if params[:path]
18   - @commits = @repo.log(@ref, params[:path], :max_count => params[:limit] || 100, :skip => params[:offset] || 0)
  19 + @commits = @repo.log(@ref, params[:path], :max_count => limit, :skip => offset)
19 20 else
20   - @commits = @repo.commits(@ref, params[:limit] || 100, params[:offset] || 0)
  21 + @commits = @repo.commits(@ref, limit, offset)
21 22 end
22 23  
23 24 respond_to do |format|
... ... @@ -28,12 +29,12 @@ class CommitsController &lt; ApplicationController
28 29  
29 30 def show
30 31 @commit = project.repo.commits(params[:id]).first
31   - @notes = project.notes.where(:noteable_id => @commit.id, :noteable_type => "Commit")
  32 + @notes = project.notes.where(:noteable_id => @commit.id, :noteable_type => "Commit").order("created_at DESC").limit(20)
32 33 @note = @project.notes.new(:noteable_id => @commit.id, :noteable_type => "Commit")
33 34  
34   - respond_to do |format|
35   - format.html # show.html.erb
36   - format.js
  35 + respond_to do |format|
  36 + format.html
  37 + format.js { respond_with_notes }
37 38 end
38 39 end
39 40 end
... ...
app/controllers/issues_controller.rb
... ... @@ -35,8 +35,13 @@ class IssuesController &lt; ApplicationController
35 35 end
36 36  
37 37 def show
38   - @notes = @issue.notes.order("created_at ASC")
  38 + @notes = @issue.notes.order("created_at DESC").limit(20)
39 39 @note = @project.notes.new(:noteable => @issue)
  40 +
  41 + respond_to do |format|
  42 + format.html
  43 + format.js { respond_with_notes }
  44 + end
40 45 end
41 46  
42 47 def create
... ...
app/controllers/projects_controller.rb
... ... @@ -10,7 +10,9 @@ class ProjectsController &lt; ApplicationController
10 10 before_filter :require_non_empty_project, :only => [:blob, :tree]
11 11  
12 12 def index
13   - @projects = current_user.projects.all
  13 + source = current_user.projects
  14 + source = source.tagged_with(params[:tag]) unless params[:tag].blank?
  15 + @projects = source.all
14 16 end
15 17  
16 18 def new
... ... @@ -86,13 +88,12 @@ class ProjectsController &lt; ApplicationController
86 88 def wall
87 89 @note = Note.new
88 90 @notes = @project.common_notes.order("created_at DESC")
  91 + @notes = @notes.fresh.limit(20)
89 92  
90   - @notes = case params[:view]
91   - when "week" then @notes.since((Date.today - 7.days).at_beginning_of_day)
92   - when "all" then @notes.all
93   - when "day" then @notes.since(Date.today.at_beginning_of_day)
94   - else @notes.fresh.limit(10)
95   - end
  93 + respond_to do |format|
  94 + format.html
  95 + format.js { respond_with_notes }
  96 + end
96 97 end
97 98  
98 99 #
... ...
app/controllers/tags_controller.rb 0 → 100644
... ... @@ -0,0 +1,11 @@
  1 +class TagsController < ApplicationController
  2 + def index
  3 + @tags = Project.tag_counts.order('count DESC')
  4 + @tags = @tags.where('name like ?', "%#{params[:term]}%") unless params[:term].blank?
  5 +
  6 + respond_to do |format|
  7 + format.html
  8 + format.json { render json: @tags.limit(8).map {|t| t.name}}
  9 + end
  10 + end
  11 +end
... ...
app/helpers/application_helper.rb
... ... @@ -4,6 +4,18 @@ module ApplicationHelper
4 4 "http://www.gravatar.com/avatar/#{Digest::MD5.hexdigest(user_email)}?s=40&d=identicon"
5 5 end
6 6  
  7 + def fixed_mode?
  8 + @view_mode == :fixed
  9 + end
  10 +
  11 + def body_class(default_class = nil)
  12 + main = content_for(:body_class).blank? ?
  13 + default_class :
  14 + content_for(:body_class)
  15 +
  16 + [main, cookies[:view_style]].join(" ")
  17 + end
  18 +
7 19 def commit_name(project, commit)
8 20 if project.commit.id == commit.id
9 21 "master"
... ... @@ -32,6 +44,15 @@ module ApplicationHelper
32 44 "Never"
33 45 end
34 46  
  47 + def grouped_options_refs
  48 + options = [
  49 + ["Branch", @repo.heads.map(&:name) ],
  50 + [ "Tag", @project.tags ]
  51 + ]
  52 +
  53 + grouped_options_for_select(options, @ref)
  54 + end
  55 +
35 56 def markdown(text)
36 57 RDiscount.new(text, :autolink, :no_pseudo_protocols, :safelink, :smart, :filter_html).to_html.html_safe
37 58 end
... ...
app/models/key.rb
... ... @@ -8,7 +8,7 @@ class Key &lt; ActiveRecord::Base
8 8 validates :key,
9 9 :presence => true,
10 10 :uniqueness => true,
11   - :length => { :within => 0..1024 }
  11 + :length => { :within => 0..1600 }
12 12  
13 13 before_save :set_identifier
14 14 after_save :update_gitosis
... ...
app/models/project.rb
... ... @@ -9,6 +9,8 @@ class Project &lt; ActiveRecord::Base
9 9 has_many :notes, :dependent => :destroy
10 10 has_many :snippets, :dependent => :destroy
11 11  
  12 + acts_as_taggable
  13 +
12 14 validates :name,
13 15 :uniqueness => true,
14 16 :presence => true,
... ... @@ -48,6 +50,11 @@ class Project &lt; ActiveRecord::Base
48 50 code
49 51 end
50 52  
  53 + def team_member_by_name_or_email(email = nil, name = nil)
  54 + user = users.where("email like ? or name like ?", email, name).first
  55 + users_projects.find_by_user_id(user.id) if user
  56 + end
  57 +
51 58 def common_notes
52 59 notes.where(:noteable_type => ["", nil])
53 60 end
... ...
app/views/commits/_commits.html.haml
... ... @@ -17,9 +17,8 @@
17 17 = image_tag "no_avatar.png", :class => "left", :width => 40, :style => "padding-right:5px;"
18 18 %span.commit-title
19 19 %strong
20   - = truncate(commit.safe_message, :length => 60)
  20 + = truncate(commit.safe_message, :length => fixed_mode? ? 60 : 120)
21 21 %span.commit-author
22 22 %strong= commit.author_name
23 23 = time_ago_in_words(commit.committed_date)
24 24 ago
25   -= more_commits_link if @commits.size > 99
... ...
app/views/commits/index.html.haml
1 1 - content_for(:body_class, "project-page commits-page")
2 2  
3   -.left
4   - = form_tag project_commits_path(@project), :method => :get do
5   - = select_tag "branch", options_for_select(@repo.heads.map(&:name), @branch), :onchange => "this.form.submit();", :class => "", :prompt => "Branches"
6   -.left
7   - = form_tag project_commits_path(@project), :method => :get do
8   - = select_tag "tag", options_for_select(@project.tags, @tag), :onchange => "this.form.submit();", :class => "", :prompt => "Tags"
9   -.clear
10   -
11   -%br
12   -
13   -
14 3 -#%a.right.button{:href => "#"} Download
15 4 -#-if can? current_user, :admin_project, @project
16 5 %a.right.button.blue{:href => "#"} EDIT
17 6 %h2.icon
18 7 %span
19   - %a.project-name{:href => "#"}
20   - %i.arrow>
21   - Project
22   - &nbsp;
23 8 %d
24   - %a{:href => "#"}
25   - = @ref
26   - - if params[:path]
27   - &nbsp;
28   - %d
  9 + = link_to project_commits_path(@project) do
  10 + = @project.name
  11 + - if params[:path]
  12 + \/
29 13 %a{:href => "#"}= params[:path].split("/").join(" / ")
30 14  
  15 +.right= render :partial => "projects/refs", :locals => { :destination => project_commits_path(@project) }
31 16  
32 17 %div{:id => dom_id(@project)}
33   - = render "commits"
  18 + #commits_list= render "commits"
  19 +.clear
  20 +.loading{ :style => "display:none;"}
  21 + %center= image_tag "ajax-loader.gif"
  22 +
  23 +
  24 +
  25 +:javascript
  26 + $(function(){
  27 + CommitsList.init("#{@ref}", 20);
  28 + });
... ...
app/views/commits/index.js.erb
... ... @@ -1,2 +0,0 @@
1   -$("#more-commits-link").remove();
2   -$('#<%= dom_id(@project)%>').append('<%= escape_javascript(render("commits")) %>');
app/views/commits/index.js.haml 0 → 100644
... ... @@ -0,0 +1,3 @@
  1 +:plain
  2 + CommitsList.append(#{@commits.count}, "#{escape_javascript(render(:partial => 'commits/commits'))}");
  3 +
... ...
app/views/commits/show.html.haml
1 1 %h3
2   - = "[ #{@commit.committer} ] #{truncate(@commit.safe_message)}"
  2 + = "[ #{@commit.author_name} ] #{truncate(@commit.safe_message, :length => 70)}"
3 3 -#= link_to 'Back', project_commits_path(@project), :class => "button"
4 4 %table.round-borders
5 5 %tr
... ... @@ -9,11 +9,8 @@
9 9 %td Author
10 10 %td= @commit.author_name
11 11 %tr
12   - %td Commiter
13   - %td= @commit.committer
14   - %tr
15 12 %td Commited Date
16   - %td= @commit.committed_date
  13 + %td= @commit.committed_date.stamp("21 Aug 2011, 11:15pm")
17 14 %tr
18 15 %td Message
19 16 %td
... ... @@ -24,18 +21,7 @@
24 21 %td= link_to 'Browse Code', tree_project_path(@project, :commit_id => @commit.id)
25 22 .clear
26 23  
27   -#tabs
28   - %ul
29   - %li
30   - %a{ :href => "#tabs-1" } Diff
31   - %li
32   - %a{ :href => "#tabs-2" } Comments
33   - %span{ :class => "notes_count" }= @notes.count
34   - %hr
35   - #tabs-1
36   - = render "commits/diff"
37   - #tabs-2
38   - = render "notes/notes"
  24 +%br
39 25  
40   -:javascript
41   - $(function() { $( "#tabs" ).tabs(); });
  26 += render "commits/diff"
  27 += render "notes/notes"
... ...
app/views/commits/show.js.haml
1   --#:plain
2   - $("#side-commit-preview").remove();
3   - var side = $("<div id='side-commit-preview'></div>");
4   - side.html("#{escape_javascript(render "commits/show")}");
5   - $("##{dom_id(@project)}").parent().append(side);
6   - $("##{dom_id(@project)}").addClass("span-14");
7   -:plain
8   - $("#notes-list").html("#{escape_javascript(render(:partial => 'notes/notes_list'))}");
  1 += render "notes/load"
... ...
app/views/issues/_issues.html.haml
... ... @@ -6,7 +6,6 @@
6 6 %th ID
7 7 %th Title
8 8 %th Closed?
9   - %th
10 9  
11 10 - @issues.critical.each do |issue|
12 11 = render(:partial => 'show', :locals => {:issue => issue})
... ...
app/views/issues/_show.html.haml
... ... @@ -4,15 +4,24 @@
4 4 = image_tag "move.png" , :class => [:handle, :left]
5 5 %td
6 6 = image_tag gravatar_icon(issue.assignee.email), :class => "left", :width => 40, :style => "padding:0 5px;"
7   - = truncate issue.assignee.name, :lenght => 20
  7 + = issue.assignee.name
8 8 %td ##{issue.id}
9 9 %td
10   - = truncate(html_escape(issue.title), :length => 60)
  10 + = truncate(html_escape(issue.title), :length => 200)
  11 + %br
11 12 %br
12 13 - if issue.critical
13 14 %span.tag.high critical
14 15 - if issue.today?
15 16 %span.tag.today today
  17 +
  18 + .right
  19 + - if can?(current_user, :admin_issue, @project) || issue.author == current_user
  20 + = link_to 'Edit', edit_project_issue_path(@project, issue), :class => "cgray", :remote => true
  21 + - if can?(current_user, :admin_issue, @project) || issue.author == current_user
  22 + &nbsp;
  23 + = link_to 'Destroy', [@project, issue], :confirm => 'Are you sure?', :method => :delete, :remote => true, :class => "cred delete-issue negative", :id => "destroy_issue_#{issue.id}"
  24 +
16 25 -#- if issue.author == current_user
17 26 -#%span.tag.yours yours
18 27 -#- if issue.notes.count > 0
... ... @@ -26,8 +35,3 @@
26 35 = hidden_field_tag :status_only, true
27 36 - else
28 37 = check_box_tag "closed", 1, issue.closed, :disabled => true
29   - %td
30   - - if can?(current_user, :admin_issue, @project) || issue.author == current_user
31   - = link_to 'Edit', edit_project_issue_path(@project, issue), :class => "lbutton positive", :remote => true
32   - - if can?(current_user, :admin_issue, @project) || issue.author == current_user
33   - = link_to 'Destroy', [@project, issue], :confirm => 'Are you sure?', :method => :delete, :remote => true, :class => "lbutton delete-issue negative", :id => "destroy_issue_#{issue.id}"
... ...
app/views/issues/index.html.haml
... ... @@ -7,18 +7,18 @@
7 7 = hidden_field_tag :project_id, @project.id, { :id => 'project_id' }
8 8 = search_field_tag :issue_search, nil, { :placeholder => 'Search', :class => 'issue_search' }
9 9  
10   - .right
  10 + .right.issues_filter
11 11 = form_tag project_issues_path(@project), :method => :get do
12   - .span-2
  12 + .left
13 13 = radio_button_tag :f, 0, (params[:f] || "0") == "0", :onclick => "this.form.submit()", :id => "open_issues", :class => "status"
14 14 = label_tag "open_issues","Open"
15   - .span-2
  15 + .left
16 16 = radio_button_tag :f, 2, params[:f] == "2", :onclick => "this.form.submit()", :id => "closed_issues", :class => "status"
17 17 = label_tag "closed_issues","Closed"
18   - .span-2
  18 + .left
19 19 = radio_button_tag :f, 3, params[:f] == "3", :onclick => "this.form.submit()", :id => "my_issues", :class => "status"
20 20 = label_tag "my_issues","To Me"
21   - .span-2
  21 + .left
22 22 = radio_button_tag :f, 1, params[:f] == "1", :onclick => "this.form.submit()", :id => "all_issues", :class => "status"
23 23 = label_tag "all_issues","All"
24 24  
... ...
app/views/issues/show.html.haml
1 1 %h2
2   - = "Issue ##{@issue.id} - #{html_escape(@issue.title)}"
  2 + %strong
  3 + Issue
  4 + = "##{@issue.id}"
  5 + &ndash;
  6 + = html_escape(@issue.title)
3 7 .left.width-65p
4   - -#= simple_format html_escape(@issue.content)
5 8 .issue_notes= render "notes/notes"
  9 +
  10 + .loading{ :style => "display:none;"}
  11 + %center= image_tag "ajax-loader.gif"
6 12 .right.width-30p
7 13 .span-8
8   - - if @issue.closed
9   - %center.success Closed
10   - - else
11   - %center.error Open
12 14 %table.round-borders
13 15 %tr
14   - %td Title:
15   - %td
16   - = truncate html_escape(@issue.title)
17   - %tr
18   - %td Project
19   - %td
20   - %strong= @issue.project.name
21   - %tr
22 16 %td Author:
23 17 %td
24 18 = image_tag gravatar_icon(@issue.author.email), :class => "left", :width => 40, :style => "padding:0 5px;"
... ... @@ -41,7 +35,7 @@
41 35 %tr
42 36 %td Closed?
43 37 %td
44   - - if can? current_user, :write_issue, @project
  38 + - if can? current_user, :write_issue, @issue
45 39 = form_for([@project, @issue]) do |f|
46 40 = f.check_box :closed, :onclick => "$(this).parent().submit();"
47 41 = hidden_field_tag :status_only, true
... ... @@ -49,9 +43,9 @@
49 43 = check_box_tag "closed", 1, @issue.closed, :disabled => true
50 44  
51 45  
52   - - if can?(current_user, :admin_issue, @issue)
  46 + - if can?(current_user, :write_issue, @issue)
53 47 .clear
54   - = link_to 'Edit', edit_project_issue_path(@project, @issue), :class => "lbutton positive", :remote => true
55   - .right= link_to 'Destroy', [@project, @issue], :confirm => 'Are you sure?', :method => :delete, :class => "lbutton delete-issue negative", :id => "destroy_issue_#{@issue.id}"
  48 + %br
  49 + = link_to 'Edit', edit_project_issue_path(@project, @issue), :class => "lbutton positive", :remote => true
  50 + .right= link_to 'Destroy', [@project, @issue], :confirm => 'Are you sure?', :method => :delete, :class => "lbutton delete-issue negative", :id => "destroy_issue_#{@issue.id}"
56 51 .clear
57   -
... ...
app/views/issues/show.js.haml
1   -:plain
2   - $("#notes-list").html("#{escape_javascript(render(:partial => 'notes/notes_list'))}");
  1 += render "notes/load"
... ...
app/views/layouts/_head_panel.html.erb
1 1 <!-- Page Header -->
2 2 <header>
3   -<h1 class="logo">
4   - <a href="/">GITLAB</a></h1>
5   - <div class="login-top">
6   - <%= link_to profile_path, :class => "pic" do %>
7   - <%= image_tag gravatar_icon(current_user.email) %>
8   - <% end %>
9   - <%= link_to profile_path, :class => "username" do %>
10   - <%= current_user.name %>
11   - <% end %>
12   - <%= link_to 'Logout', destroy_user_session_path, :class => "logout", :method => :delete %>
  3 + <h1 class="logo">
  4 + <a href="/">GITLAB</a>
  5 + </h1>
  6 + <div class="account-box">
  7 + <%= link_to profile_path, :class => "pic" do %>
  8 + <%= image_tag gravatar_icon(current_user.email) %>
  9 + <% end %>
  10 +
  11 + <a href="#" class="arrow-up"></a>
  12 +
  13 + <div class="account-links">
  14 + <%= link_to profile_path, :class => "username" do %>
  15 + <%#= current_user.name %>
  16 + Your profile
  17 + <% end %>
  18 + <%= link_to "Fluid layout", url_for( :view_style => 'fluid' ) if cookies[:view_style] == "collapsed"%>
  19 + <%= link_to "Fixed layout", url_for( :view_style => 'collapsed' ) unless cookies[:view_style] == "collapsed"%>
  20 + <%= link_to 'Logout', destroy_user_session_path, :class => "logout", :method => :delete %>
13 21 </div>
14   - <div class="search">
15   - <%= text_field_tag "search", nil, :placeholder => "Search", :class => "search-input" %>
16   - </div>
  22 + </div><!-- .account-box -->
  23 +
  24 + <div class="search">
  25 + <%= text_field_tag "search", nil, :placeholder => "Search", :class => "search-input" %>
  26 + </div>
17 27 <!-- .login-top -->
18 28 <nav>
19 29 <%= link_to dashboard_path, :class => current_page?(root_path) ? "current dashboard" : "dashboard" do %>
... ... @@ -26,23 +36,10 @@
26 36 <span></span>Admin
27 37 <% end %>
28 38 </nav>
  39 +
29 40 </header>
30 41 <!-- eo Page Header -->
31 42  
32   -<div id="header-panel" style="display:none">
33   - <div class="container">
34   - <div class="span-24">
35   - <div class="span-10">
36   - <span class="search-holder">
37   - </span>
38   - </div>
39   - <div class="right">
40   - <%= link_to truncate(@project.name, :length => 20), project_path(@project), :class => "current button" if @project && !@project.new_record? %>
41   - </div>
42   - </div>
43   - </div>
44   -</div>
45   -
46 43 <% if current_user %>
47 44 <%= javascript_tag do %>
48 45 $(function() {
... ...
app/views/layouts/admin.html.haml
... ... @@ -6,11 +6,10 @@
6 6 = stylesheet_link_tag "application"
7 7 = javascript_include_tag "application"
8 8 = csrf_meta_tags
9   - %link{:href => "/assets/favicon.png", :rel => "icon", :type => "image/png"}/
10 9 = javascript_tag do
11 10 REQ_URI = "#{request.env["REQUEST_URI"]}";
12 11 REQ_REFFER = "#{request.env["HTTP_REFERER"]}";
13   - %body{ :class => content_for?(:body_class) ? yield(:body_class) : 'project-page', :id => yield(:boyd_id)}
  12 + %body{ :class => body_class('project-page'), :id => yield(:boyd_id)}
14 13 #container
15 14 = render :partial => "layouts/flash"
16 15 = render :partial => "layouts/head_panel"
... ...
app/views/layouts/application.html.haml
... ... @@ -2,22 +2,16 @@
2 2 %html
3 3 %head
4 4 %title
5   - GitLab #{" - #{@project.name}" if @project && !@project.new_record?}
6   - -#= stylesheet_link_tag 'blueprint/screen', :media => "screen, projection"
7   - -#= stylesheet_link_tag 'blueprint/print', :media => "print"
8   - -#= stylesheet_link_tag 'blueprint/plugins/buttons/screen', :media => "screen, projection"
9   - -#= stylesheet_link_tag 'blueprint/plugins/link-icons/screen', :media => "screen, projection"
  5 + GitLab
10 6 = stylesheet_link_tag "application"
11 7 = javascript_include_tag "application"
12 8 = csrf_meta_tags
13   - %link{:href => "/assets/favicon.png", :rel => "icon", :type => "image/png"}/
14 9 = javascript_tag do
15 10 REQ_URI = "#{request.env["REQUEST_URI"]}";
16 11 REQ_REFFER = "#{request.env["HTTP_REFERER"]}";
17   - %body{ :class => yield(:body_class), :id => yield(:boyd_id)}
  12 + %body{ :class => body_class, :id => yield(:boyd_id)}
18 13 #container
19 14 = render :partial => "layouts/flash"
20 15 = render :partial => "layouts/head_panel"
21   - %div{ :id => "main", :role => "main", :class => "container_4" }
22   - = render :partial => "layouts/page_title"
23   - = yield
  16 + = render :partial => "layouts/page_title"
  17 + = yield
... ...
app/views/layouts/devise.html.haml
... ... @@ -6,7 +6,6 @@
6 6 = stylesheet_link_tag "application"
7 7 = javascript_include_tag "application"
8 8 = csrf_meta_tags
9   - %link{:href => "/assets/favicon.png", :rel => "icon", :type => "image/png"}/
10 9 = javascript_tag do
11 10 REQ_URI = "#{request.env["REQUEST_URI"]}";
12 11 REQ_REFFER = "#{request.env["HTTP_REFERER"]}";
... ...
app/views/layouts/profile.html.haml
... ... @@ -6,11 +6,10 @@
6 6 = stylesheet_link_tag "application"
7 7 = javascript_include_tag "application"
8 8 = csrf_meta_tags
9   - %link{:href => "/assets/favicon.png", :rel => "icon", :type => "image/png"}/
10 9 = javascript_tag do
11 10 REQ_URI = "#{request.env["REQUEST_URI"]}";
12 11 REQ_REFFER = "#{request.env["HTTP_REFERER"]}";
13   - %body{ :class => content_for?(:body_class) ? yield(:body_class) : 'project-page', :id => yield(:boyd_id)}
  12 + %body{ :class => body_class('project-page'), :id => yield(:boyd_id)}
14 13 #container
15 14 = render :partial => "layouts/flash"
16 15 = render :partial => "layouts/head_panel"
... ...
app/views/layouts/project.html.haml
... ... @@ -6,40 +6,39 @@
6 6 = stylesheet_link_tag "application"
7 7 = javascript_include_tag "application"
8 8 = csrf_meta_tags
9   - %link{:href => "/assets/favicon.png", :rel => "icon", :type => "image/png"}/
10 9 = javascript_tag do
11 10 REQ_URI = "#{request.env["REQUEST_URI"]}";
12 11 REQ_REFFER = "#{request.env["HTTP_REFERER"]}";
13   - %body{ :class => content_for?(:body_class) ? yield(:body_class) : 'project-page', :id => yield(:boyd_id)}
  12 + %body{ :class => body_class('project-page'), :id => yield(:boyd_id)}
14 13 #container
15 14 = render :partial => "layouts/flash"
16 15 = render :partial => "layouts/head_panel"
17 16 .project-container
18 17 .project-sidebar
19 18 .fixed
20   - %input.git-url.text{:id => "", :name => "", :readonly => "", :type => "text", :value => @project.url_to_repo}
21   - %aside
22   - = link_to "History", project_path(@project), :class => current_page?(:controller => "projects", :action => "show", :id => @project) ? "current" : nil
23   - = link_to "Tree", tree_project_path(@project), :class => current_page?(:controller => "projects", :action => "tree", :id => @project) ? "current" : nil
24   - = link_to "Commits", project_commits_path(@project), :class => current_page?(:controller => "commits", :action => "index", :project_id => @project) ? "current" : nil
25   - = link_to team_project_path(@project), :class => (current_page?(:controller => "projects", :action => "team", :id => @project) || controller.controller_name == "team_members") ? "current" : nil do
26   - Team
27   - - if @project.users_projects.count > 0
28   - %span{ :class => "number" }= @project.users_projects.count
29   - = link_to project_issues_path(@project), :class => (controller.controller_name == "issues") ? "current" : nil do
30   - Issues
31   - - if @project.issues.opened.count > 0
32   - %span{ :class => "number" }= @project.issues.opened.count
33   - = link_to wall_project_path(@project), :class => current_page?(:controller => "projects", :action => "wall", :id => @project) ? "current" : nil do
34   - Wall
35   - - if @project.common_notes.count > 0
36   - %span{ :class => "number" }= @project.common_notes.count
37   - = link_to project_snippets_path(@project), :class => (controller.controller_name == "snippets") ? "current" : nil do
38   - Snippets
39   - - if @project.snippets.count > 0
40   - %span{ :class => "number" }= @project.snippets.non_expired.count
41   - - if @commit
42   - = link_to truncate(commit_name(@project,@commit), :length => 15), project_commit_path(@project, :id => @commit.id), :class => current_page?(:controller => "commits", :action => "show", :project_id => @project, :id => @commit.id) ? "current" : nil
  19 + %input.git-url.text{:id => "", :name => "", :readonly => "", :type => "text", :value => @project.url_to_repo, :class => "one_click_select"}
  20 + %aside
  21 + = link_to "History", project_path(@project), :class => current_page?(:controller => "projects", :action => "show", :id => @project) ? "current" : nil
  22 + = link_to "Tree", tree_project_path(@project), :class => current_page?(:controller => "projects", :action => "tree", :id => @project) ? "current" : nil
  23 + = link_to "Commits", project_commits_path(@project), :class => current_page?(:controller => "commits", :action => "index", :project_id => @project) ? "current" : nil
  24 + = link_to team_project_path(@project), :class => (current_page?(:controller => "projects", :action => "team", :id => @project) || controller.controller_name == "team_members") ? "current" : nil do
  25 + Team
  26 + - if @project.users_projects.count > 0
  27 + %span{ :class => "number" }= @project.users_projects.count
  28 + = link_to project_issues_path(@project), :class => (controller.controller_name == "issues") ? "current" : nil do
  29 + Issues
  30 + - if @project.issues.opened.count > 0
  31 + %span{ :class => "number" }= @project.issues.opened.count
  32 + = link_to wall_project_path(@project), :class => current_page?(:controller => "projects", :action => "wall", :id => @project) ? "current" : nil do
  33 + Wall
  34 + - if @project.common_notes.count > 0
  35 + %span{ :class => "number" }= @project.common_notes.count
  36 + = link_to project_snippets_path(@project), :class => (controller.controller_name == "snippets") ? "current" : nil do
  37 + Snippets
  38 + - if @project.snippets.count > 0
  39 + %span{ :class => "number" }= @project.snippets.non_expired.count
  40 + - if @commit
  41 + = link_to truncate(commit_name(@project,@commit), :length => 15), project_commit_path(@project, :id => @commit.id), :class => current_page?(:controller => "commits", :action => "show", :project_id => @project, :id => @commit.id) ? "current" : nil
43 42  
44 43 .project-content
45 44 = yield
... ...
app/views/notes/_load.js.haml 0 → 100644
... ... @@ -0,0 +1,17 @@
  1 +- unless @notes.blank?
  2 +
  3 + - if params[:last_id] && params[:first_id]
  4 + :plain
  5 + NoteList.replace(#{@notes.last.id}, #{@notes.first.id}, "#{escape_javascript(render(:partial => 'notes/notes_list'))}");
  6 +
  7 +
  8 + - elsif params[:last_id]
  9 + :plain
  10 + NoteList.prepend(#{@notes.first.id}, "#{escape_javascript(render(:partial => 'notes/notes_list'))}");
  11 +
  12 + - elsif params[:first_id]
  13 + :plain
  14 + NoteList.append(#{@notes.last.id}, "#{escape_javascript(render(:partial => 'notes/notes_list'))}");
  15 +
  16 + - else
  17 + :plain
... ...
app/views/notes/_notes.html.haml
1   -- if controller.action_name == "wall"
2   - %ul#notes-list= render "notes/notes_list"
3   -
4   -- else
5   - %ul#notes-list= render "notes/notes_list"
6   - %br
7   - %br
8   - - if can? current_user, :write_note, @project
9   - = render "notes/form"
  1 +- if can? current_user, :write_note, @project
  2 + = render "notes/form"
  3 +.clear
  4 +%hr
  5 +%ul#notes-list= render "notes/notes_list"
10 6  
11 7 :javascript
12 8 $('.delete-note').live('ajax:success', function() {
... ... @@ -20,8 +16,11 @@
20 16 $("#submit_note").removeAttr("disabled");
21 17 })
22 18  
23   -- if ["issues", "projects"].include?(controller.controller_name)
24   - :javascript
25   - $(function(){
26   - var int =self.setInterval("updatePage()", 20000);
  19 + $(function(){
  20 + $("#note_note").live("click", function(){
  21 + $(this).css("height", "100px");
  22 + $('.attach_holder').show();
27 23 });
  24 +
  25 + NoteList.init("wall", #{@notes.last.try(:id) || 0}, #{@notes.first.try(:id) || 0});
  26 + });
... ...
app/views/notes/_show.html.haml
1   -%li{:id => dom_id(note)}
2   - %div.note_author
3   - = image_tag gravatar_icon(note.author.email), :class => "left", :width => 40, :style => "padding-right:5px;"
4   - %div.note_content.left
5   - = markdown(note.note)
6   - - if note.attachment.url
7   - Attachment:
8   - = link_to note.attachment_identifier, note.attachment.url, :target => "_blank"
9   - %br
10   - %span.author= note.author.name
11   - %cite.ago
  1 +%li{:id => dom_id(note), :class => "note"}
  2 + = image_tag gravatar_icon(note.author.email), :class => "left", :width => 40, :style => "padding-right:5px;"
  3 + %div.note-author
  4 + %strong= note.author_name
  5 + %cite.cgray
12 6 = time_ago_in_words(note.updated_at)
13 7 ago
14   - %br
15   - - if(note.author_id == current_user.id) || can?(current_user, :admin_note, @project)
16   - = link_to 'Remove', [@project, note], :confirm => 'Are you sure?', :method => :delete, :remote => true, :class => "lbutton delete-note right negative"
  8 + - if(note.author_id == current_user.id) || can?(current_user, :admin_note, @project)
  9 + = link_to "Remove", [@project, note], :confirm => 'Are you sure?', :method => :delete, :remote => true, :class => "cred delete-note right"
  10 +
  11 + %div.note-title
  12 + = markdown(note.note)
  13 + - if note.attachment.url
  14 + .right
  15 + %span.file
  16 + = link_to note.attachment_identifier, note.attachment.url, :target => "_blank"
17 17 .clear
... ...
app/views/notes/create.js.haml
1 1 - if @note.valid?
2 2 :plain
3 3 $("#new_note .errors").remove();
4   - updatePage();
5 4 $('#note_note').val("");
  5 + NoteList.prepend(#{@note.id}, "#{escape_javascript(render :partial => "notes/show", :locals => {:note => @note})}");
6 6 - else
7 7 :plain
8 8 $("#new_note").replaceWith("#{escape_javascript(render('form'))}");
... ...
app/views/projects/_form.html.haml
... ... @@ -25,6 +25,11 @@
25 25 .left= f.label :code
26 26 %cite.right http://yourserver/
27 27 %td= f.text_field :code, :placeholder => "example"
  28 +
  29 + %tr
  30 + %td= f.label :tag_list
  31 + %td= f.text_area :tag_list, :placeholder => "project tags", :style => "height:50px", :id => :tag_field
  32 +
28 33 .field
29 34 = f.label :description
30 35 %br/
... ... @@ -41,8 +46,25 @@
41 46 %h3.prepend-top Creating project &amp; repository. Please wait for few minutes
42 47 - else
43 48 %h3.prepend-top Updating project &amp; repository. Please wait for few minutes
  49 +
44 50 :javascript
45 51 $('.new_project, .edit_project').bind('ajax:before', function() {
46 52 $(this).find(".form_content").hide();
47 53 $('.ajax_loader').show();
48 54 });
  55 +
  56 +:javascript
  57 + $(function(){
  58 + var tag_field = $('#tag_field').tagify();
  59 +
  60 + tag_field.tagify('inputField').autocomplete({
  61 + source: '/tags.json'
  62 + });
  63 +
  64 +
  65 + $('form').submit( function() {
  66 + var tag_field = $('#tag_field')
  67 + tag_field.val( tag_field.tagify('serialize') );
  68 + return true;
  69 + });
  70 + })
... ...
app/views/projects/_list.html.haml
... ... @@ -10,7 +10,12 @@
10 10  
11 11 - @projects.each do |project|
12 12 %tr{ :class => "project", :url => project_path(project) }
13   - %td= project.name
  13 + %td
  14 + = project.name
  15 + .small-tags
  16 + - project.tag_list.each do |tag|
  17 + = link_to tag, "/tags/#{tag}"
  18 +
14 19 %td= truncate project.url_to_repo
15 20 %td= project.code
16 21 %td= check_box_tag "read", 1, project.readers.include?(current_user), :disabled => :disabled
... ...
app/views/projects/_refs.html.haml 0 → 100644
... ... @@ -0,0 +1,8 @@
  1 += form_tag destination, :method => :get, :class => "project-refs-form" do
  2 + = select_tag "ref", grouped_options_refs, :onchange => "this.form.submit();", :class => "project-refs-select"
  3 +
  4 +
  5 +:javascript
  6 + $(function(){
  7 + $('.project-refs-select').chosen();
  8 + })
... ...
app/views/projects/_tile.html.haml
... ... @@ -12,6 +12,11 @@
12 12 %span Last Activity:
13 13 - last_note = project.notes.last
14 14 = last_note ? last_note.created_at.stamp("24 Aug, 2011") : "Never"
  15 +
  16 + %p.small-tags
  17 + - project.tag_list.each do |tag|
  18 + = link_to tag, "/tags/#{tag}"
  19 +
15 20 .buttons
16 21 %a.browse-code.button.yellow{:href => tree_project_path(project)} Browse code
17 22 %a.commits.button.green{:href => project_commits_path(project)} Commits
... ...
app/views/projects/_tree.html.haml
1   -.left
2   - = form_tag tree_project_path(@project), :method => :get do
3   - = select_tag "branch", options_for_select(@repo.heads.map(&:name), @branch), :onchange => "this.form.submit();", :class => "", :prompt => "Branches"
4   -.left
5   - = form_tag tree_project_path(@project), :method => :get do
6   - = select_tag "tag", options_for_select(@project.tags, @tag), :onchange => "this.form.submit();", :class => "", :prompt => "Tags"
7   -.clear
8   -
9   -%br
10 1  
11 2 -#%a.right.button{:href => "#"} Download
12 3 -#-if can? current_user, :admin_project, @project
... ... @@ -14,14 +5,9 @@
14 5 #tree-breadcrumbs
15 6 %h2.icon
16 7 %span
17   - = link_to tree_project_path(@project, :path => nil, :commit_id => @commit.try(:id)), :remote => true, :class => 'project-name' do
18   - %i.arrow>
19   - = @project.name
20   - &nbsp;
21 8 %d
22   - %a{:href => "#"}
23   - = @ref
24   -
  9 + = link_to tree_project_path(@project, :path => nil, :commit_id => @commit.try(:id)), :remote => true do
  10 + = @project.name
25 11 - if params[:path]
26 12 - part_path = ""
27 13 - params[:path].split("\/").each do |part|
... ... @@ -30,7 +16,8 @@
30 16 - part_path = part
31 17 \/
32 18 = link_to truncate(part, :length => 40), tree_file_project_path(@project, :path => part_path, :commit_id => @commit.try(:id), :branch => @branch, :tag => @tag), :remote => :true
33   -
  19 + &nbsp;
  20 + .right= render :partial => "projects/refs", :locals => { :destination => tree_project_path(@project) }
34 21 .clear
35 22  
36 23 #tree-content-holder
... ...
app/views/projects/_tree_item.html.haml
... ... @@ -11,5 +11,8 @@
11 11 %td
12 12 = time_ago_in_words(content_commit.committed_date)
13 13 ago
14   - %td
15   - = link_to truncate(content_commit.safe_message, :length => 40), project_commit_path(@project, content_commit)
  14 + %td.commit
  15 + = link_to truncate(content_commit.safe_message, :length => fixed_mode? ? 40 : 80), project_commit_path(@project, content_commit), :class => "tree-commit-link"
  16 + - tm = @project.team_member_by_name_or_email(content_commit.author_email, content_commit.author_name)
  17 + - if tm
  18 + = link_to "[#{tm.user_name}]", project_team_member_path(@project, tm)
... ...
app/views/projects/index.html.haml
1 1 - content_for(:body_class, "projects-page")
2 2 - content_for(:page_title) do
3   - .grid_4
4   - - if current_user.can_create_project?
5   - %a.grey-button.right{:href => new_project_path} Create new project
6   - %h2.icon
7   - %span
8   - Projects
  3 + .container_4
  4 + .grid_4
  5 + - if current_user.can_create_project?
  6 + %a.grey-button.right{:href => new_project_path} Create new project
  7 + %h2.icon
  8 + %span
  9 + Projects
9 10  
10   -- unless @projects.empty?
11   - %div{:class => "tile", :style => view_mode_style("tile")}
12   - = render "tile"
13   - %div{:class => "list", :style => view_mode_style("list")}
14   - = render "list"
15   -- else
16   - %center.prepend-top
17   - %h2
18   - %cite Nothing here
  11 + %div.clear
  12 + - unless @projects.empty?
  13 + %div{:class => "tile", :style => view_mode_style("tile")}
  14 + = render "tile"
  15 + %div{:class => "list", :style => view_mode_style("list")}
  16 + = render "list"
  17 + - else
  18 + %center.prepend-top
  19 + %h2
  20 + %cite Nothing here
... ...
app/views/projects/wall.html.haml
1 1 %div.wall_page
2   - - if can? current_user, :write_note, @project
3   - = render "notes/form"
4   - .right
5   - = form_tag wall_project_path(@project), :method => :get do
6   - .span-2
7   - = radio_button_tag :view, "recent", (params[:view] || "recent") == "recent", :onclick => "this.form.submit()", :id => "recent_view"
8   - = label_tag "recent_view","Recent"
9   - .span-2
10   - = radio_button_tag :view, "day", params[:view] == "day", :onclick => "this.form.submit()", :id => "day_view"
11   - = label_tag "day_view","Today"
12   - .span-2
13   - = radio_button_tag :view, "week", params[:view] == "week", :onclick => "this.form.submit()", :id => "week_view"
14   - = label_tag "week_view","Week"
15   - .span-2
16   - = radio_button_tag :view, "all", params[:view] == "all", :onclick => "this.form.submit()", :id => "all_view"
17   - = label_tag "all_view","All"
18   - .clear
19   - %br
20   - %hr
21   -= render "notes/notes"
  2 + = render "notes/notes"
  3 +
  4 +.loading{ :style => "display:none;"}
  5 + %center= image_tag "ajax-loader.gif"
22 6  
23   -:javascript
24   - $(function(){
25   - $("#note_note").live("click", function(){
26   - $(this).css("height", "100px");
27   - $('.attach_holder').show();
28   - });
29   - });
... ...
app/views/projects/wall.js.haml
1   -:plain
2   - $("#notes-list").html("#{escape_javascript(render(:partial => 'notes/notes_list'))}");
  1 += render "notes/load"
... ...
app/views/tags/index.html.haml 0 → 100644
... ... @@ -0,0 +1,11 @@
  1 +- content_for(:body_class, "projects-page")
  2 +- content_for(:page_title) do
  3 + .grid_4
  4 + %h2
  5 + Tags
  6 +
  7 +
  8 + .tags-list
  9 + - @tags.all.each do |tag|
  10 + = link_to "#{tag.name}(#{tag.count})", "/tags/#{tag.name}"
  11 +
... ...
config/routes.rb
1 1 Gitlab::Application.routes.draw do
  2 +
  3 + get 'tags'=> 'tags#index'
  4 + get 'tags/:tag' => 'projects#index'
  5 +
  6 +
2 7 namespace :admin do
3 8 resources :users
4 9 resources :projects
... ... @@ -20,6 +25,7 @@ Gitlab::Application.routes.draw do
20 25  
21 26 resources :projects, :only => [:new, :create, :index]
22 27 resources :keys
  28 +
23 29 devise_for :users
24 30  
25 31 resources :projects, :except => [:new, :create, :index], :path => "/" do
... ...
db/migrate/20111101222453_acts_as_taggable_on_migration.rb 0 → 100644
... ... @@ -0,0 +1,28 @@
  1 +class ActsAsTaggableOnMigration < ActiveRecord::Migration
  2 + def self.up
  3 + create_table :tags do |t|
  4 + t.string :name
  5 + end
  6 +
  7 + create_table :taggings do |t|
  8 + t.references :tag
  9 +
  10 + # You should make sure that the column created is
  11 + # long enough to store the required class names.
  12 + t.references :taggable, :polymorphic => true
  13 + t.references :tagger, :polymorphic => true
  14 +
  15 + t.string :context
  16 +
  17 + t.datetime :created_at
  18 + end
  19 +
  20 + add_index :taggings, :tag_id
  21 + add_index :taggings, [:taggable_id, :taggable_type, :context]
  22 + end
  23 +
  24 + def self.down
  25 + drop_table :taggings
  26 + drop_table :tags
  27 + end
  28 +end
... ...
db/schema.rb
... ... @@ -11,7 +11,7 @@
11 11 #
12 12 # It's strongly recommended to check this file into your version control system.
13 13  
14   -ActiveRecord::Schema.define(:version => 20111027152724) do
  14 +ActiveRecord::Schema.define(:version => 20111101222453) do
15 15  
16 16 create_table "issues", :force => true do |t|
17 17 t.string "title"
... ... @@ -68,6 +68,23 @@ ActiveRecord::Schema.define(:version =&gt; 20111027152724) do
68 68 t.datetime "expires_at"
69 69 end
70 70  
  71 + create_table "taggings", :force => true do |t|
  72 + t.integer "tag_id"
  73 + t.integer "taggable_id"
  74 + t.string "taggable_type"
  75 + t.integer "tagger_id"
  76 + t.string "tagger_type"
  77 + t.string "context"
  78 + t.datetime "created_at"
  79 + end
  80 +
  81 + add_index "taggings", ["tag_id"], :name => "index_taggings_on_tag_id"
  82 + add_index "taggings", ["taggable_id", "taggable_type", "context"], :name => "index_taggings_on_taggable_id_and_taggable_type_and_context"
  83 +
  84 + create_table "tags", :force => true do |t|
  85 + t.string "name"
  86 + end
  87 +
71 88 create_table "users", :force => true do |t|
72 89 t.string "email", :default => "", :null => false
73 90 t.string "encrypted_password", :limit => 128, :default => "", :null => false
... ...
spec/requests/commits_notes_spec.rb
... ... @@ -12,7 +12,6 @@ describe &quot;Issues&quot; do
12 12 describe "add new note", :js => true do
13 13 before do
14 14 visit project_commit_path(project, commit)
15   - click_link "Comments" # notes tab
16 15 fill_in "note_note", :with => "I commented this commit"
17 16 click_button "Add note"
18 17 end
... ...
spec/requests/tags_spec.rb 0 → 100644
... ... @@ -0,0 +1,31 @@
  1 +require 'spec_helper'
  2 +
  3 +describe "Tags" do
  4 + before { login_as :user }
  5 +
  6 + # describe "GET 'tags/index'" do
  7 + # it "should be successful" do
  8 + # get 'tags/index'
  9 + # response.should be_success
  10 + # end
  11 + # end
  12 +
  13 +
  14 + describe "GET '/tags.json'" do
  15 + before do
  16 + @project = Factory :project
  17 + @project.add_access(@user, :read)
  18 + @project.tag_list = 'demo1'
  19 + @project.save
  20 + visit '/tags.json'
  21 + end
  22 +
  23 +
  24 + it "should contains tags" do
  25 + page.should have_content('demo1')
  26 + end
  27 +end
  28 +
  29 +
  30 +
  31 +end
... ...
vendor/assets/javascripts/jquery.tagify.js 0 → 100644
... ... @@ -0,0 +1,143 @@
  1 +/* Author: Alicia Liu */
  2 +
  3 +(function ($) {
  4 +
  5 + $.widget("ui.tagify", {
  6 + options: {
  7 + delimiters: [13, 188], // what user can type to complete a tag in char codes: [enter], [comma]
  8 + outputDelimiter: ',', // delimiter for tags in original input field
  9 + cssClass: 'tagify-container', // CSS class to style the tagify div and tags, see stylesheet
  10 + addTagPrompt: 'add tags' // placeholder text
  11 + },
  12 +
  13 + _create: function() {
  14 + var self = this,
  15 + el = self.element,
  16 + opts = self.options;
  17 +
  18 + this.tags = [];
  19 +
  20 + // hide text field and replace with a div that contains it's own input field for entering tags
  21 + this.tagInput = $("<input type='text'>")
  22 + .attr( 'placeholder', opts.addTagPrompt )
  23 + .keypress( function(e) {
  24 + var $this = $(this),
  25 + pressed = e.which;
  26 +
  27 + for ( i in opts.delimiters ) {
  28 +
  29 + if (pressed == opts.delimiters[i]) {
  30 + self.add( $this.val() );
  31 + e.preventDefault();
  32 + return false;
  33 + }
  34 + }
  35 + })
  36 + // for some reason, in Safari, backspace is only recognized on keyup
  37 + .keyup( function(e) {
  38 + var $this = $(this),
  39 + pressed = e.which;
  40 +
  41 + // if backspace is hit with no input, remove the last tag
  42 + if (pressed == 8) { // backspace
  43 + if ( $this.val() == "" ) {
  44 + self.remove();
  45 + return false;
  46 + }
  47 + return;
  48 + }
  49 + });
  50 +
  51 + this.tagDiv = $("<div></div>")
  52 + .addClass( opts.cssClass )
  53 + .click( function() {
  54 + $(this).children('input').focus();
  55 + })
  56 + .append( this.tagInput )
  57 + .insertAfter( el.hide() );
  58 +
  59 + // if the field isn't empty, parse the field for tags, and prepopulate existing tags
  60 + var initVal = $.trim( el.val() );
  61 +
  62 + if ( initVal ) {
  63 + var initTags = initVal.split( opts.outputDelimiter );
  64 + $.each( initTags, function(i, tag) {
  65 + self.add( tag );
  66 + });
  67 + }
  68 + },
  69 +
  70 + _setOption: function( key, value ) {
  71 + options.key = value;
  72 + },
  73 +
  74 + // add a tag, public function
  75 + add: function(text) {
  76 + var self = this;
  77 + text = text || self.tagInput.val();
  78 + if (text) {
  79 + var tagIndex = self.tags.length;
  80 +
  81 + var removeButton = $("<a href='#'>x</a>")
  82 + .click( function() {
  83 + self.remove( tagIndex );
  84 + return false;
  85 + });
  86 + var newTag = $("<span></span>")
  87 + .text( text )
  88 + .append( removeButton );
  89 +
  90 + self.tagInput.before( newTag );
  91 + self.tags.push( text );
  92 + self.tagInput.val('');
  93 + }
  94 + },
  95 +
  96 + // remove a tag by index, public function
  97 + // if index is blank, remove the last tag
  98 + remove: function( tagIndex ) {
  99 + var self = this;
  100 + if ( tagIndex == null || tagIndex === (self.tags.length - 1) ) {
  101 + this.tagDiv.children("span").last().remove();
  102 + self.tags.pop();
  103 + }
  104 + if ( typeof(tagIndex) == 'number' ) {
  105 + // otherwise just hide this tag, and we don't mess up the index
  106 + this.tagDiv.children( "span:eq(" + tagIndex + ")" ).hide();
  107 + // we rely on the serialize function to remove null values
  108 + delete( self.tags[tagIndex] );
  109 + }
  110 + },
  111 +
  112 + // serialize the tags with the given delimiter, and write it back into the tagified field
  113 + serialize: function() {
  114 + var self = this;
  115 + var delim = self.options.outputDelimiter;
  116 + var tagsStr = self.tags.join( delim );
  117 +
  118 + // our tags might have deleted entries, remove them here
  119 + var dupes = new RegExp(delim + delim + '+', 'g'); // regex: /,,+/g
  120 + var ends = new RegExp('^' + delim + '|' + delim + '$', 'g'); // regex: /^,|,$/g
  121 + var outputStr = tagsStr.replace( dupes, delim ).replace(ends, '');
  122 +
  123 + self.element.val(outputStr);
  124 + return outputStr;
  125 + },
  126 +
  127 + inputField: function() {
  128 + return this.tagInput;
  129 + },
  130 +
  131 + containerDiv: function() {
  132 + return this.tagDiv;
  133 + },
  134 +
  135 + // remove the div, and show original input
  136 + destroy: function() {
  137 + $.Widget.prototype.destroy.apply(this);
  138 + this.tagDiv.remove();
  139 + this.element.show();
  140 + }
  141 + });
  142 +
  143 +})(jQuery);
0 144 \ No newline at end of file
... ...
vendor/assets/stylesheets/jquery-ui/jquery.tagify.css 0 → 100644
... ... @@ -0,0 +1,34 @@
  1 +/* Tagify styles
  2 +Author: Alicia Liu test
  3 +*/
  4 +
  5 +.tagify-container {
  6 +}
  7 +
  8 +.tagify-container > span {
  9 + display: inline-block;
  10 + padding: 8px 11px 8px 11px;
  11 + margin: 1px 5px 0px 0px;
  12 + border-radius: 4px;
  13 + border: 1px solid #d0e1ff;
  14 + background-color: #d0e1ff;
  15 + color: #0f326d;
  16 + font-weight: bold;
  17 + font-size: 14px;
  18 +}
  19 +
  20 +.tagify-container > span > a {
  21 + padding-left: 5px !important;
  22 + color: #83a5e1;
  23 + text-decoration: none;
  24 + font-weight: bold;
  25 +}
  26 +
  27 +.tagify-container > input {
  28 + border: 0 none;
  29 + width: 100px !important;
  30 +}
  31 +
  32 +.tagify-container > input:focus {
  33 + outline: none;
  34 +}
0 35 \ No newline at end of file
... ...