Revision 1298:4f746d8966dd .svn/pristine/3b
| .svn/pristine/3b/3b0a04b81b3503971b4e570669380aa1831579cc.svn-base | ||
|---|---|---|
| 1 |
require File.expand_path('../../test_helper', __FILE__)
|
|
| 2 |
require 'search_controller' |
|
| 3 |
|
|
| 4 |
# Re-raise errors caught by the controller. |
|
| 5 |
class SearchController; def rescue_action(e) raise e end; end |
|
| 6 |
|
|
| 7 |
class SearchControllerTest < ActionController::TestCase |
|
| 8 |
fixtures :projects, :enabled_modules, :roles, :users, :members, :member_roles, |
|
| 9 |
:issues, :trackers, :issue_statuses, |
|
| 10 |
:custom_fields, :custom_values, |
|
| 11 |
:repositories, :changesets |
|
| 12 |
|
|
| 13 |
def setup |
|
| 14 |
@controller = SearchController.new |
|
| 15 |
@request = ActionController::TestRequest.new |
|
| 16 |
@response = ActionController::TestResponse.new |
|
| 17 |
User.current = nil |
|
| 18 |
end |
|
| 19 |
|
|
| 20 |
def test_search_for_projects |
|
| 21 |
get :index |
|
| 22 |
assert_response :success |
|
| 23 |
assert_template 'index' |
|
| 24 |
|
|
| 25 |
get :index, :q => "cook" |
|
| 26 |
assert_response :success |
|
| 27 |
assert_template 'index' |
|
| 28 |
assert assigns(:results).include?(Project.find(1)) |
|
| 29 |
end |
|
| 30 |
|
|
| 31 |
def test_search_all_projects |
|
| 32 |
get :index, :q => 'recipe subproject commit', :all_words => '' |
|
| 33 |
assert_response :success |
|
| 34 |
assert_template 'index' |
|
| 35 |
|
|
| 36 |
assert assigns(:results).include?(Issue.find(2)) |
|
| 37 |
assert assigns(:results).include?(Issue.find(5)) |
|
| 38 |
assert assigns(:results).include?(Changeset.find(101)) |
|
| 39 |
assert_tag :dt, :attributes => { :class => /issue/ },
|
|
| 40 |
:child => { :tag => 'a', :content => /Add ingredients categories/ },
|
|
| 41 |
:sibling => { :tag => 'dd', :content => /should be classified by categories/ }
|
|
| 42 |
|
|
| 43 |
assert assigns(:results_by_type).is_a?(Hash) |
|
| 44 |
assert_equal 5, assigns(:results_by_type)['changesets'] |
|
| 45 |
assert_tag :a, :content => 'Changesets (5)' |
|
| 46 |
end |
|
| 47 |
|
|
| 48 |
def test_search_issues |
|
| 49 |
get :index, :q => 'issue', :issues => 1 |
|
| 50 |
assert_response :success |
|
| 51 |
assert_template 'index' |
|
| 52 |
|
|
| 53 |
assert_equal true, assigns(:all_words) |
|
| 54 |
assert_equal false, assigns(:titles_only) |
|
| 55 |
assert assigns(:results).include?(Issue.find(8)) |
|
| 56 |
assert assigns(:results).include?(Issue.find(5)) |
|
| 57 |
assert_tag :dt, :attributes => { :class => /issue closed/ },
|
|
| 58 |
:child => { :tag => 'a', :content => /Closed/ }
|
|
| 59 |
end |
|
| 60 |
|
|
| 61 |
def test_search_project_and_subprojects |
|
| 62 |
get :index, :id => 1, :q => 'recipe subproject', :scope => 'subprojects', :all_words => '' |
|
| 63 |
assert_response :success |
|
| 64 |
assert_template 'index' |
|
| 65 |
assert assigns(:results).include?(Issue.find(1)) |
|
| 66 |
assert assigns(:results).include?(Issue.find(5)) |
|
| 67 |
end |
|
| 68 |
|
|
| 69 |
def test_search_without_searchable_custom_fields |
|
| 70 |
CustomField.update_all "searchable = #{ActiveRecord::Base.connection.quoted_false}"
|
|
| 71 |
|
|
| 72 |
get :index, :id => 1 |
|
| 73 |
assert_response :success |
|
| 74 |
assert_template 'index' |
|
| 75 |
assert_not_nil assigns(:project) |
|
| 76 |
|
|
| 77 |
get :index, :id => 1, :q => "can" |
|
| 78 |
assert_response :success |
|
| 79 |
assert_template 'index' |
|
| 80 |
end |
|
| 81 |
|
|
| 82 |
def test_search_with_searchable_custom_fields |
|
| 83 |
get :index, :id => 1, :q => "stringforcustomfield" |
|
| 84 |
assert_response :success |
|
| 85 |
results = assigns(:results) |
|
| 86 |
assert_not_nil results |
|
| 87 |
assert_equal 1, results.size |
|
| 88 |
assert results.include?(Issue.find(7)) |
|
| 89 |
end |
|
| 90 |
|
|
| 91 |
def test_search_all_words |
|
| 92 |
# 'all words' is on by default |
|
| 93 |
get :index, :id => 1, :q => 'recipe updating saving', :all_words => '1' |
|
| 94 |
assert_equal true, assigns(:all_words) |
|
| 95 |
results = assigns(:results) |
|
| 96 |
assert_not_nil results |
|
| 97 |
assert_equal 1, results.size |
|
| 98 |
assert results.include?(Issue.find(3)) |
|
| 99 |
end |
|
| 100 |
|
|
| 101 |
def test_search_one_of_the_words |
|
| 102 |
get :index, :id => 1, :q => 'recipe updating saving', :all_words => '' |
|
| 103 |
assert_equal false, assigns(:all_words) |
|
| 104 |
results = assigns(:results) |
|
| 105 |
assert_not_nil results |
|
| 106 |
assert_equal 3, results.size |
|
| 107 |
assert results.include?(Issue.find(3)) |
|
| 108 |
end |
|
| 109 |
|
|
| 110 |
def test_search_titles_only_without_result |
|
| 111 |
get :index, :id => 1, :q => 'recipe updating saving', :titles_only => '1' |
|
| 112 |
results = assigns(:results) |
|
| 113 |
assert_not_nil results |
|
| 114 |
assert_equal 0, results.size |
|
| 115 |
end |
|
| 116 |
|
|
| 117 |
def test_search_titles_only |
|
| 118 |
get :index, :id => 1, :q => 'recipe', :titles_only => '1' |
|
| 119 |
assert_equal true, assigns(:titles_only) |
|
| 120 |
results = assigns(:results) |
|
| 121 |
assert_not_nil results |
|
| 122 |
assert_equal 2, results.size |
|
| 123 |
end |
|
| 124 |
|
|
| 125 |
def test_search_content |
|
| 126 |
Issue.update_all("description = 'This is a searchkeywordinthecontent'", "id=1")
|
|
| 127 |
|
|
| 128 |
get :index, :id => 1, :q => 'searchkeywordinthecontent', :titles_only => '' |
|
| 129 |
assert_equal false, assigns(:titles_only) |
|
| 130 |
results = assigns(:results) |
|
| 131 |
assert_not_nil results |
|
| 132 |
assert_equal 1, results.size |
|
| 133 |
end |
|
| 134 |
|
|
| 135 |
def test_search_with_invalid_project_id |
|
| 136 |
get :index, :id => 195, :q => 'recipe' |
|
| 137 |
assert_response 404 |
|
| 138 |
assert_nil assigns(:results) |
|
| 139 |
end |
|
| 140 |
|
|
| 141 |
def test_quick_jump_to_issue |
|
| 142 |
# issue of a public project |
|
| 143 |
get :index, :q => "3" |
|
| 144 |
assert_redirected_to '/issues/3' |
|
| 145 |
|
|
| 146 |
# issue of a private project |
|
| 147 |
get :index, :q => "4" |
|
| 148 |
assert_response :success |
|
| 149 |
assert_template 'index' |
|
| 150 |
end |
|
| 151 |
|
|
| 152 |
def test_large_integer |
|
| 153 |
get :index, :q => '4615713488' |
|
| 154 |
assert_response :success |
|
| 155 |
assert_template 'index' |
|
| 156 |
end |
|
| 157 |
|
|
| 158 |
def test_tokens_with_quotes |
|
| 159 |
get :index, :id => 1, :q => '"good bye" hello "bye bye"' |
|
| 160 |
assert_equal ["good bye", "hello", "bye bye"], assigns(:tokens) |
|
| 161 |
end |
|
| 162 |
end |
|
| .svn/pristine/3b/3b1b10ce6290c2ed3a8ab7a0cbea04c53b0b08e9.svn-base | ||
|---|---|---|
| 1 |
/* redMine - project management software |
|
| 2 |
Copyright (C) 2006-2008 Jean-Philippe Lang */ |
|
| 3 |
|
|
| 4 |
var observingContextMenuClick; |
|
| 5 |
|
|
| 6 |
ContextMenu = Class.create(); |
|
| 7 |
ContextMenu.prototype = {
|
|
| 8 |
initialize: function (url) {
|
|
| 9 |
this.url = url; |
|
| 10 |
this.createMenu(); |
|
| 11 |
|
|
| 12 |
if (!observingContextMenuClick) {
|
|
| 13 |
Event.observe(document, 'click', this.Click.bindAsEventListener(this)); |
|
| 14 |
Event.observe(document, 'contextmenu', this.RightClick.bindAsEventListener(this)); |
|
| 15 |
observingContextMenuClick = true; |
|
| 16 |
} |
|
| 17 |
|
|
| 18 |
this.unselectAll(); |
|
| 19 |
this.lastSelected = null; |
|
| 20 |
}, |
|
| 21 |
|
|
| 22 |
RightClick: function(e) {
|
|
| 23 |
this.hideMenu(); |
|
| 24 |
// do not show the context menu on links |
|
| 25 |
if (Event.element(e).tagName == 'A') { return; }
|
|
| 26 |
var tr = Event.findElement(e, 'tr'); |
|
| 27 |
if (tr == document || tr == undefined || !tr.hasClassName('hascontextmenu')) { return; }
|
|
| 28 |
Event.stop(e); |
|
| 29 |
if (!this.isSelected(tr)) {
|
|
| 30 |
this.unselectAll(); |
|
| 31 |
this.addSelection(tr); |
|
| 32 |
this.lastSelected = tr; |
|
| 33 |
} |
|
| 34 |
this.showMenu(e); |
|
| 35 |
}, |
|
| 36 |
|
|
| 37 |
Click: function(e) {
|
|
| 38 |
this.hideMenu(); |
|
| 39 |
if (Event.element(e).tagName == 'A' || Event.element(e).tagName == 'IMG') { return; }
|
|
| 40 |
if (Event.isLeftClick(e) || (navigator.appVersion.match(/\bMSIE\b/))) {
|
|
| 41 |
var tr = Event.findElement(e, 'tr'); |
|
| 42 |
if (tr!=null && tr!=document && tr.hasClassName('hascontextmenu')) {
|
|
| 43 |
// a row was clicked, check if the click was on checkbox |
|
| 44 |
var box = Event.findElement(e, 'input'); |
|
| 45 |
if (box!=document && box!=undefined) {
|
|
| 46 |
// a checkbox may be clicked |
|
| 47 |
if (box.checked) {
|
|
| 48 |
tr.addClassName('context-menu-selection');
|
|
| 49 |
} else {
|
|
| 50 |
tr.removeClassName('context-menu-selection');
|
|
| 51 |
} |
|
| 52 |
} else {
|
|
| 53 |
if (e.ctrlKey || e.metaKey) {
|
|
| 54 |
this.toggleSelection(tr); |
|
| 55 |
} else if (e.shiftKey) {
|
|
| 56 |
if (this.lastSelected != null) {
|
|
| 57 |
var toggling = false; |
|
| 58 |
var rows = $$('.hascontextmenu');
|
|
| 59 |
for (i=0; i<rows.length; i++) {
|
|
| 60 |
if (toggling || rows[i]==tr) {
|
|
| 61 |
this.addSelection(rows[i]); |
|
| 62 |
} |
|
| 63 |
if (rows[i]==tr || rows[i]==this.lastSelected) {
|
|
| 64 |
toggling = !toggling; |
|
| 65 |
} |
|
| 66 |
} |
|
| 67 |
} else {
|
|
| 68 |
this.addSelection(tr); |
|
| 69 |
} |
|
| 70 |
} else {
|
|
| 71 |
this.unselectAll(); |
|
| 72 |
this.addSelection(tr); |
|
| 73 |
} |
|
| 74 |
this.lastSelected = tr; |
|
| 75 |
} |
|
| 76 |
} else {
|
|
| 77 |
// click is outside the rows |
|
| 78 |
var t = Event.findElement(e, 'a'); |
|
| 79 |
if (t == document || t == undefined) {
|
|
| 80 |
this.unselectAll(); |
|
| 81 |
} else {
|
|
| 82 |
if (Element.hasClassName(t, 'disabled') || Element.hasClassName(t, 'submenu')) {
|
|
| 83 |
Event.stop(e); |
|
| 84 |
} |
|
| 85 |
} |
|
| 86 |
} |
|
| 87 |
} |
|
| 88 |
}, |
|
| 89 |
|
|
| 90 |
createMenu: function() {
|
|
| 91 |
if (!$('context-menu')) {
|
|
| 92 |
var menu = document.createElement("div");
|
|
| 93 |
menu.setAttribute("id", "context-menu");
|
|
| 94 |
menu.setAttribute("style", "display:none;");
|
|
| 95 |
document.getElementById("content").appendChild(menu);
|
|
| 96 |
} |
|
| 97 |
}, |
|
| 98 |
|
|
| 99 |
showMenu: function(e) {
|
|
| 100 |
var mouse_x = Event.pointerX(e); |
|
| 101 |
var mouse_y = Event.pointerY(e); |
|
| 102 |
var render_x = mouse_x; |
|
| 103 |
var render_y = mouse_y; |
|
| 104 |
var dims; |
|
| 105 |
var menu_width; |
|
| 106 |
var menu_height; |
|
| 107 |
var window_width; |
|
| 108 |
var window_height; |
|
| 109 |
var max_width; |
|
| 110 |
var max_height; |
|
| 111 |
|
|
| 112 |
$('context-menu').style['left'] = (render_x + 'px');
|
|
| 113 |
$('context-menu').style['top'] = (render_y + 'px');
|
|
| 114 |
Element.update('context-menu', '');
|
|
| 115 |
|
|
| 116 |
new Ajax.Updater({success:'context-menu'}, this.url,
|
|
| 117 |
{asynchronous:true,
|
|
| 118 |
method: 'get', |
|
| 119 |
evalScripts:true, |
|
| 120 |
parameters:Form.serialize(Event.findElement(e, 'form')), |
|
| 121 |
onComplete:function(request){
|
|
| 122 |
dims = $('context-menu').getDimensions();
|
|
| 123 |
menu_width = dims.width; |
|
| 124 |
menu_height = dims.height; |
|
| 125 |
max_width = mouse_x + 2*menu_width; |
|
| 126 |
max_height = mouse_y + menu_height; |
|
| 127 |
|
|
| 128 |
var ws = window_size(); |
|
| 129 |
window_width = ws.width; |
|
| 130 |
window_height = ws.height; |
|
| 131 |
|
|
| 132 |
/* display the menu above and/or to the left of the click if needed */ |
|
| 133 |
if (max_width > window_width) {
|
|
| 134 |
render_x -= menu_width; |
|
| 135 |
$('context-menu').addClassName('reverse-x');
|
|
| 136 |
} else {
|
|
| 137 |
$('context-menu').removeClassName('reverse-x');
|
|
| 138 |
} |
|
| 139 |
if (max_height > window_height) {
|
|
| 140 |
render_y -= menu_height; |
|
| 141 |
$('context-menu').addClassName('reverse-y');
|
|
| 142 |
} else {
|
|
| 143 |
$('context-menu').removeClassName('reverse-y');
|
|
| 144 |
} |
|
| 145 |
if (render_x <= 0) render_x = 1; |
|
| 146 |
if (render_y <= 0) render_y = 1; |
|
| 147 |
$('context-menu').style['left'] = (render_x + 'px');
|
|
| 148 |
$('context-menu').style['top'] = (render_y + 'px');
|
|
| 149 |
|
|
| 150 |
Effect.Appear('context-menu', {duration: 0.20});
|
|
| 151 |
if (window.parseStylesheets) { window.parseStylesheets(); } // IE
|
|
| 152 |
}}) |
|
| 153 |
}, |
|
| 154 |
|
|
| 155 |
hideMenu: function() {
|
|
| 156 |
Element.hide('context-menu');
|
|
| 157 |
}, |
|
| 158 |
|
|
| 159 |
addSelection: function(tr) {
|
|
| 160 |
tr.addClassName('context-menu-selection');
|
|
| 161 |
this.checkSelectionBox(tr, true); |
|
| 162 |
this.clearDocumentSelection(); |
|
| 163 |
}, |
|
| 164 |
|
|
| 165 |
toggleSelection: function(tr) {
|
|
| 166 |
if (this.isSelected(tr)) {
|
|
| 167 |
this.removeSelection(tr); |
|
| 168 |
} else {
|
|
| 169 |
this.addSelection(tr); |
|
| 170 |
} |
|
| 171 |
}, |
|
| 172 |
|
|
| 173 |
removeSelection: function(tr) {
|
|
| 174 |
tr.removeClassName('context-menu-selection');
|
|
| 175 |
this.checkSelectionBox(tr, false); |
|
| 176 |
}, |
|
| 177 |
|
|
| 178 |
unselectAll: function() {
|
|
| 179 |
var rows = $$('.hascontextmenu');
|
|
| 180 |
for (i=0; i<rows.length; i++) {
|
|
| 181 |
this.removeSelection(rows[i]); |
|
| 182 |
} |
|
| 183 |
}, |
|
| 184 |
|
|
| 185 |
checkSelectionBox: function(tr, checked) {
|
|
| 186 |
var inputs = Element.getElementsBySelector(tr, 'input'); |
|
| 187 |
if (inputs.length > 0) { inputs[0].checked = checked; }
|
|
| 188 |
}, |
|
| 189 |
|
|
| 190 |
isSelected: function(tr) {
|
|
| 191 |
return Element.hasClassName(tr, 'context-menu-selection'); |
|
| 192 |
}, |
|
| 193 |
|
|
| 194 |
clearDocumentSelection: function() {
|
|
| 195 |
if (document.selection) {
|
|
| 196 |
document.selection.clear(); // IE |
|
| 197 |
} else {
|
|
| 198 |
window.getSelection().removeAllRanges(); |
|
| 199 |
} |
|
| 200 |
} |
|
| 201 |
} |
|
| 202 |
|
|
| 203 |
function toggleIssuesSelection(el) {
|
|
| 204 |
var boxes = el.getElementsBySelector('input[type=checkbox]');
|
|
| 205 |
var all_checked = true; |
|
| 206 |
for (i = 0; i < boxes.length; i++) { if (boxes[i].checked == false) { all_checked = false; } }
|
|
| 207 |
for (i = 0; i < boxes.length; i++) {
|
|
| 208 |
if (all_checked) {
|
|
| 209 |
boxes[i].checked = false; |
|
| 210 |
boxes[i].up('tr').removeClassName('context-menu-selection');
|
|
| 211 |
} else if (boxes[i].checked == false) {
|
|
| 212 |
boxes[i].checked = true; |
|
| 213 |
boxes[i].up('tr').addClassName('context-menu-selection');
|
|
| 214 |
} |
|
| 215 |
} |
|
| 216 |
} |
|
| 217 |
|
|
| 218 |
function window_size() {
|
|
| 219 |
var w; |
|
| 220 |
var h; |
|
| 221 |
if (window.innerWidth) {
|
|
| 222 |
w = window.innerWidth; |
|
| 223 |
h = window.innerHeight; |
|
| 224 |
} else if (document.documentElement) {
|
|
| 225 |
w = document.documentElement.clientWidth; |
|
| 226 |
h = document.documentElement.clientHeight; |
|
| 227 |
} else {
|
|
| 228 |
w = document.body.clientWidth; |
|
| 229 |
h = document.body.clientHeight; |
|
| 230 |
} |
|
| 231 |
return {width: w, height: h};
|
|
| 232 |
} |
|
| .svn/pristine/3b/3b35aa3e53757a7316cb173ab97cf120044ce7fe.svn-base | ||
|---|---|---|
| 1 |
<%= yield %> (with plugin layout) |
|
| .svn/pristine/3b/3b77726a8efda3938653233bf84d8e02401c4bc3.svn-base | ||
|---|---|---|
| 1 |
# Redmine - project management software |
|
| 2 |
# Copyright (C) 2006-2013 Jean-Philippe Lang |
|
| 3 |
# |
|
| 4 |
# This program is free software; you can redistribute it and/or |
|
| 5 |
# modify it under the terms of the GNU General Public License |
|
| 6 |
# as published by the Free Software Foundation; either version 2 |
|
| 7 |
# of the License, or (at your option) any later version. |
|
| 8 |
# |
|
| 9 |
# This program is distributed in the hope that it will be useful, |
|
| 10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
| 11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
| 12 |
# GNU General Public License for more details. |
|
| 13 |
# |
|
| 14 |
# You should have received a copy of the GNU General Public License |
|
| 15 |
# along with this program; if not, write to the Free Software |
|
| 16 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
| 17 |
|
|
| 18 |
class WelcomeController < ApplicationController |
|
| 19 |
caches_action :robots |
|
| 20 |
|
|
| 21 |
def index |
|
| 22 |
@news = News.latest User.current |
|
| 23 |
@projects = Project.latest User.current |
|
| 24 |
end |
|
| 25 |
|
|
| 26 |
def robots |
|
| 27 |
@projects = Project.all_public.active |
|
| 28 |
render :layout => false, :content_type => 'text/plain' |
|
| 29 |
end |
|
| 30 |
end |
|
| .svn/pristine/3b/3bdc9be57907c77b3244c04ca134e43fe57a10c8.svn-base | ||
|---|---|---|
| 1 |
<%= call_hook(:view_issues_form_details_top, { :issue => @issue, :form => f }) %>
|
|
| 2 |
|
|
| 3 |
<div id="issue_descr_fields" <%= 'style="display:none"' unless @issue.new_record? || @issue.errors.any? %>> |
|
| 4 |
<% if @issue.safe_attribute_names.include?('is_private') %>
|
|
| 5 |
<p style="float:right; margin-right:1em;"> |
|
| 6 |
<label class="inline" for="issue_is_private" id="issue_is_private_label"><%= f.check_box :is_private, :no_label => true %> <%= l(:field_is_private) %></label> |
|
| 7 |
</p> |
|
| 8 |
<% end %> |
|
| 9 |
<p><%= f.select :tracker_id, @project.trackers.collect {|t| [t.name, t.id]}, :required => true %></p>
|
|
| 10 |
<%= observe_field :issue_tracker_id, :url => { :action => :new, :project_id => @project, :id => @issue },
|
|
| 11 |
:update => :attributes, |
|
| 12 |
:with => "Form.serialize('issue-form')" %>
|
|
| 13 |
|
|
| 14 |
<p><%= f.text_field :subject, :size => 80, :required => true %></p> |
|
| 15 |
<p><%= f.text_area :description, |
|
| 16 |
:cols => 60, |
|
| 17 |
:rows => (@issue.description.blank? ? 10 : [[10, @issue.description.length / 50].max, 100].min), |
|
| 18 |
:accesskey => accesskey(:edit), |
|
| 19 |
:class => 'wiki-edit' %></p> |
|
| 20 |
</div> |
|
| 21 |
|
|
| 22 |
<div id="attributes" class="attributes"> |
|
| 23 |
<%= render :partial => 'issues/attributes' %> |
|
| 24 |
</div> |
|
| 25 |
|
|
| 26 |
<% if @issue.new_record? %> |
|
| 27 |
<p id="attachments_form"><%= label_tag('attachments[1][file]', l(:label_attachment_plural))%><%= render :partial => 'attachments/form' %></p>
|
|
| 28 |
<% end %> |
|
| 29 |
|
|
| 30 |
<% if @issue.new_record? && User.current.allowed_to?(:add_issue_watchers, @project) -%> |
|
| 31 |
<p id="watchers_form"><label><%= l(:label_issue_watchers) %></label> |
|
| 32 |
<% @issue.project.users.sort.each do |user| -%> |
|
| 33 |
<label class="floating"><%= check_box_tag 'issue[watcher_user_ids][]', user.id, @issue.watched_by?(user) %> <%=h user %></label> |
|
| 34 |
<% end -%> |
|
| 35 |
</p> |
|
| 36 |
<% end %> |
|
| 37 |
|
|
| 38 |
<%= call_hook(:view_issues_form_details_bottom, { :issue => @issue, :form => f }) %>
|
|
| 39 |
|
|
| 40 |
<%= wikitoolbar_for 'issue_description' %> |
|
Also available in: Unified diff